Mercurial > repos > adam-novak > hexagram
changeset 0:1407e3634bcf draft default tip
Uploaded r11 from test tool shed.
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/color-0.4.1.js Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/drag.svg Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/filter.svg Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/hexagram.css Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,695 @@ +/* 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; + width: 500px; +} +#search { + width: 322px; +} + +#recalculate-statistics { + padding: 0.2em; + margin-left: 10px; +} + +#calculate-set-operation { + padding: 0.2em; +} + +#inflate{ + display: none; +} + +.recalculate-throbber { + display: none; +} + +/* Set Operation Menu */ + +#set-operation { + padding: 0.2em; +} + +.set-operation-col { + visibility:hidden; + width: 25em; + overflow: visible; +} + +.set-operation-panel-holder { + visibility:hidden; + margin-left:21em; + padding-top: 2em; + overflow: hidden; + top: 4em; + width: 25em; + z-index: 100; +} + +.set-operation-panel { + text-align: center; + visibility:hidden; + height: 160px; + position: relative; + top: 0; + border-radius: 10px; + border: 1px solid gray; + background: white; + z-index: 100; +} + +.set-operation-panel-title { + visibility:hidden; + text-align: center; +} + +.set-operation-panel-contents { + visibility:hidden; + overflow-y: auto; + overflow-x: hidden; + top: 1em; +} + +#set-operations-list { +} + +.set-operation-entry { + text-align: left; + position: relative; + height: auto; + width: auto; + padding: 5px; +} + +.set-operation-value { + visibility: hidden; + width: 15em; + margin-bottom: 10px; +} + +.set-operation-layer-value { + visibility: hidden; + width: 8em; + margin-left: 50px; +} + +.compute-button { + visibility: hidden; + text-align: center; + width: 190px; + margin-left: 25%; +} +/* 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: 30em; +} + +.results-dropdown .select2-results { + max-height: 30em; +} + +.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; +} + +/* Map Layout Selection Stuff */ + +#layout-holder{ + margin-left: 0.5em; +} + +#layout-search { + width: 22em; +} + +.layout-entry { + padding-right: 20px; +} + +layout-name { + /* Force silly underscore names into shape */ + word-wrap: break-word; +} +#current-layout { + padding-top: 2.5em; + width: 700px; +} + +/* 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; +} + +/*Shortlist holders*/ +#left-columns { + width: 33%; +} +#center-columns { + margin-left: 33%; +} +#right-columns { + margin-left: 66% +} + +/* Filtering stuff */ + +.filter { + line-height: 1em; + vertical-align: center; +} +.filter-threshold, .filter-value { + display: none; + width: 5em; +} +.filter-holder { + float: left; + text-align: left; + width: 120px; +} + +.save-filter { + display: none; +} + +/* Intersection */ +.intersection { + line-height: 1em; + vertical-align: center; +} +.intersection-text { + width: 15px; + float: left; + text-align: center; +} +.intersection-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.intersection-holder { + float: left; + text-align: center; +} +.intersection-threshold, .intersection-value { + display: none; + width: 5em; +} + +/*Union*/ +.union { + line-height: 1em; + vertical-align: center; +} +.union-text{ + width: 15px; + float: left; + text-align: center; +} +.union-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.union-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.union-threshold, .union-value { + display: none; + width: 5em; +} + +/*Set Difference*/ +.set-difference { + line-height: 1em; + vertical-align: center; +} +.set-difference-text{ + width: 15px; + float: left; + text-align: center; +} +.set-difference-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.set-difference-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.set-difference-threshold, .set-difference-value { + display: none; + width: 5em; +} + +/*Symmetric Difference*/ +.symmetric-difference { + line-height: 1em; + vertical-align: center; +} +.symmetric-difference-text{ + width: 15px; + float: left; + text-align: center; +} +.symmetric-difference-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.symmetric-difference-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.symmetric-difference-threshold, .symmetric-difference-value { + display: none; + width: 5em; +} + +/*Absolute Complement*/ +.absolute-complement { + line-height: 1em; + vertical-align: center; +} +.absolute-complement-text{ + width: 40px; + float: left; + text-align: center; +} +.absolute-complement-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.absolute-complement-holder { + width: 65px; + float: left; + text-align: center; + margin-left: 10px +} +.absolute-complement-threshold, .absolute-complement-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-6ae12361157c/hexagram/hexagram.css~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,694 @@ +/* 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; + width: 500px; +} +#search { + width: 322px; +} + +#recalculate-statistics { + padding: 0.2em; + margin-left: 10px; +} + +#calculate-set-operation { + padding: 0.2em; +} + +#inflate{ +} + +.recalculate-throbber { + display: none; +} + +/* Set Operation Menu */ + +#set-operation { + padding: 0.2em; +} + +.set-operation-col { + visibility:hidden; + width: 25em; + overflow: visible; +} + +.set-operation-panel-holder { + visibility:hidden; + margin-left:21em; + padding-top: 2em; + overflow: hidden; + top: 4em; + width: 25em; + z-index: 100; +} + +.set-operation-panel { + text-align: center; + visibility:hidden; + height: 160px; + position: relative; + top: 0; + border-radius: 10px; + border: 1px solid gray; + background: white; + z-index: 100; +} + +.set-operation-panel-title { + visibility:hidden; + text-align: center; +} + +.set-operation-panel-contents { + visibility:hidden; + overflow-y: auto; + overflow-x: hidden; + top: 1em; +} + +#set-operations-list { +} + +.set-operation-entry { + text-align: left; + position: relative; + height: auto; + width: auto; + padding: 5px; +} + +.set-operation-value { + visibility: hidden; + width: 15em; + margin-bottom: 10px; +} + +.set-operation-layer-value { + visibility: hidden; + width: 8em; + margin-left: 50px; +} + +.compute-button { + visibility: hidden; + text-align: center; + width: 190px; + margin-left: 25%; +} +/* 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: 30em; +} + +.results-dropdown .select2-results { + max-height: 30em; +} + +.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; +} + +/* Map Layout Selection Stuff */ + +#layout-holder{ + margin-left: 0.5em; +} + +#layout-search { + width: 22em; +} + +.layout-entry { + padding-right: 20px; +} + +layout-name { + /* Force silly underscore names into shape */ + word-wrap: break-word; +} +#current-layout { + padding-top: 2.5em; + width: 700px; +} + +/* 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; +} + +/*Shortlist holders*/ +#left-columns { + width: 33%; +} +#center-columns { + margin-left: 33%; +} +#right-columns { + margin-left: 66% +} + +/* Filtering stuff */ + +.filter { + line-height: 1em; + vertical-align: center; +} +.filter-threshold, .filter-value { + display: none; + width: 5em; +} +.filter-holder { + float: left; + text-align: left; + width: 120px; +} + +.save-filter { + display: none; +} + +/* Intersection */ +.intersection { + line-height: 1em; + vertical-align: center; +} +.intersection-text { + width: 15px; + float: left; + text-align: center; +} +.intersection-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.intersection-holder { + float: left; + text-align: center; +} +.intersection-threshold, .intersection-value { + display: none; + width: 5em; +} + +/*Union*/ +.union { + line-height: 1em; + vertical-align: center; +} +.union-text{ + width: 15px; + float: left; + text-align: center; +} +.union-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.union-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.union-threshold, .union-value { + display: none; + width: 5em; +} + +/*Set Difference*/ +.set-difference { + line-height: 1em; + vertical-align: center; +} +.set-difference-text{ + width: 15px; + float: left; + text-align: center; +} +.set-difference-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.set-difference-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.set-difference-threshold, .set-difference-value { + display: none; + width: 5em; +} + +/*Symmetric Difference*/ +.symmetric-difference { + line-height: 1em; + vertical-align: center; +} +.symmetric-difference-text{ + width: 15px; + float: left; + text-align: center; +} +.symmetric-difference-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.symmetric-difference-holder { + float: left; + text-align: center; + margin-left: 10px; +} +.symmetric-difference-threshold, .symmetric-difference-value { + display: none; + width: 5em; +} + +/*Absolute Complement*/ +.absolute-complement { + line-height: 1em; + vertical-align: center; +} +.absolute-complement-text{ + width: 40px; + float: left; + text-align: center; +} +.absolute-complement-checkbox { + float: left; + text-align: center; + margin-left: 2px; +} +.absolute-complement-holder { + width: 65px; + float: left; + text-align: center; + margin-left: 10px +} +.absolute-complement-threshold, .absolute-complement-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-6ae12361157c/hexagram/hexagram.html Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,94 @@ +<!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="Enrich Attributes"/> + <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." /> + <img src="set.svg" id="set-operation" title="Calculate Set Operation"/> + <img src="inflate.svg" id="inflate" title="Inflate Map"/> + </div> + <div id="layout-holder"> + <input id="layout-search" type="hidden"/> + </div> + </div> + <div id="current-layout">Current Layout:</div> + </div> + <div class="set-operation-col"> + <div class="set-operation-panel-holder"> + <div class="set-operation-panel"> + <div class="set-operation-panel-title">Set Operation Paramaters:</div> + <select id="set-operations-list" onchange="update_set_operation_drop_down()"> + <option value = "0" selected = 'selected'>Select Set Operation:</option> + <option value = "1">∩ - Intersection</option> + <option value = "2">U - Union</option> + <option value = "3">\ - Set Difference</option> + <option value = "4">∆ - Symmetric Difference</option> + <option value = "5">Not:</option> + </select> + <div id="set-operations" class="set-operation-panel-contents"> + </div> + </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-6ae12361157c/hexagram/hexagram.html~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,94 @@ +<!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="Enrich Attributes"/> + <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." /> + <img src="set.svg" id="set-operation" title="Calculate Set Operation"/> + <img src="inflate.svg" id="inflate" title="Inflate Map"/> + </div> + <div id="layout-holder"> + <select id="layout-search" type="hidden"/> + </div> + </div> + <div id="current-layout">Current Layout:</div> + </div> + <div class="set-operation-col"> + <div class="set-operation-panel-holder"> + <div class="set-operation-panel"> + <div class="set-operation-panel-title">Set Operation Paramaters:</div> + <select id="set-operations-list" onchange="update_set_operation_drop_down()"> + <option value = "0" selected = 'selected'>Select Set Operation:</option> + <option value = "1">∩ - Intersection</option> + <option value = "2">U - Union</option> + <option value = "3">\ - Set Difference</option> + <option value = "4">∆ - Symmetric Difference</option> + <option value = "5">Not:</option> + </select> + <div id="set-operations" class="set-operation-panel-contents"> + </div> + </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-6ae12361157c/hexagram/hexagram.js Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,3848 @@ +// 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 is a list of the map-layour names mantained in order of entry +var layout_names = []; + +// 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 is a list of layer names whose intersection checkbox has been selected. +var shortlist_intersection = []; + +//This is the number of intersection checkboxes that have been selected. +var shortlist_intersection_num = 0; + +// This is a list of layer names whose union checkbox has been selected. +var shortlist_union = []; + +//This is the number of union checkboxes that have been selected. +var shortlist_union_num = 0; + +//This is a list of layer names whose set difference checkbox has been selected. +var shortlist_set_difference = []; + +// This is the number of set difference checkboxes that have been selected. +var shortlist_set_difference_num = 0; + +// This is a list of the layer names whose symmetric difference checkbox +// has been selected. +var shortlist_symmetric_difference = []; + +// This is the number of symmetric difference checkboxes that have been +// selected. +var shortlist_symmetric_difference_num = 0; + +// This is an array containing the layer whose absolute complement checkbox +// has been selected. +var shortlist_absolute_complement = []; + +// This is the number of absolute complement checkboxes that have been selected. +var shortlist_absolute_complement_num = 0; + +// Records number of set-operation clicks +var set_operation_clicks = 0; + +// Boolean stating whether this is the first time the set operation popup +// has been created so that "Select Layer" Default is added only once +var first_opening = true; + +// Boolean for Creating Layer from Filter +var created = false; + +// Stores the Name of Current Layer Displayed +var current_layout_name; + +// 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" +]; + +// This is a global variable that keeps track of the current Goolge Map zoom +// This is needed to keep viewing consistent across layouts +var global_zoom = 0; + +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(1250).fadeOut(1000); + + 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 + }; + + var check_layer_exists = layers[layer_name]; + + 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]; + + var data_val = layer.data; + 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.addClass("filter-image"); + 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); + + // Add a image for the save function + var save_filter = $("<img/>").attr("src", "save.svg"); + save_filter.addClass("save-filter"); + save_filter.attr("title", "Save Filter as Layer"); + + contents.append(filter_holder); + contents.append(save_filter); + + 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(); + } + + save_filter.show (); + + save_filter.button().click(function() { + // Configure Save Filter Buttons + + // Get selected value + var selected = filter_value.prop("selectedIndex"); + var value = filter_value.val(); + + var signatures = []; + + // Gather Tumor-ID Signatures with value and push to "signatures" + for (hex in polygons){ + if (layer.data[hex] == value){ + signatures.push(hex); + } + } + + // Create Layer + if (created == false) { + select_list (signatures, "user selection"); + created = true; + } + created = false; + }); + + + // Now that the right controls are there, assume they have + refresh(); + + }); + } else { + created = false; + // Hide the filtering settings + filter_value.hide(); + filter_threshold.hide(); + save_filter.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; +} + +// ____________________________________________________________________________ +// Replacement Set Operation Code +// ____________________________________________________________________________ +function get_set_operation_selection () { + // For the new dop-down GUI for set operation selection + // we neeed a function to determine which set operation is selected. + // This way we can display the appropriate divs. + + // Drop Down List & Index for Selected Element + var drop_down = document.getElementById("set-operations-list"); + var index = drop_down.selectedIndex; + var selection = drop_down.options[index]; + + return selection; +} + +function show_set_operation_drop_down () { + // Show Set Operation Drop Down Menu + document.getElementsByClassName("set-operation-col")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel")[0].style.visibility="visible"; + document.getElementById("set-operations").style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="visible"; + +} + +function hide_set_operation_drop_down () { + // Hide Set Operation Drop Down Menu + document.getElementsByClassName("set-operation-col")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel")[0].style.visibility="hidden"; + document.getElementById("set-operations").style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="hidden"; + + // Hide the Data Values for the Selected Layers + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="hidden"; + } + + // Hide the Compute Button + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "hidden"; + + // Set the "Select Layer" drop down to the default value + var list = document.getElementById("set-operations-list"); + list.selectedIndex = 0; + + var list_value = document.getElementsByClassName("set-operation-value"); + list_value[0].selectedIndex = 0; + list_value[1].selectedIndex = 0; + + // Remove all elements from drop downs holding the data values for the + // selected layers. This way there are no values presented when the user + // clicks on the set operation button to open it again. + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + +} + +function create_set_operation_ui () { + // Returns a Jquery element that is then prepended to the existing + // set theory drop-down menu + + // This holds the root element for this set operation UI + var root = $("<div/>").addClass("set-operation-entry"); + + // Add Drop Downs to hold the selected layers and and selected data values + var set_theory_value1 = $("<select/>").addClass("set-operation-value"); + var set_theory_layer_value1 = $("<select/>").addClass("set-operation-layer-value"); + var set_theory_value2 = $("<select/>").addClass("set-operation-value"); + var set_theory_layer_value2 = $("<select/>").addClass("set-operation-layer-value"); + + var compute_button = $("<input/>").attr("type", "button"); + compute_button.addClass ("compute-button"); + + // Append to Root + root.append (set_theory_value1); + root.append (set_theory_layer_value1); + root.append (set_theory_value2); + root.append (set_theory_layer_value2); + root.append (compute_button); + + return root; +} + +function update_set_operation_drop_down () { + // This is the onchange command for the drop down displaying the + // different set operation functions. It is called whenever the user changes + // the selected set operation. + + // Get the value of the set operation selection made by the user. + var selection = get_set_operation_selection(); + var value = selection.value; + // Check if the selectin value is that of one of set operation functions + if (selection.value == 1 || selection.value == 2 + || selection.value == 3 || selection.value == 4 + || selection.value == 5){ + // Make the drop downs that hold layer names and data values visible + var drop_downs = document.getElementsByClassName("set-operation-value"); + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="visible"; + } + + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="visible"; + } + + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "visible"; + compute_button[0].value = "Compute Set Operation"; + + if (first_opening == true) { + // Set the default value for the drop down, holding the selected layers + var default_value = document.createElement("option"); + default_value.text = "Select Layer 1"; + default_value.value = 0; + drop_downs[0].add(default_value); + + var default_value2 = document.createElement("option"); + default_value2.text = "Select Layer 2"; + default_value2.value = 0; + drop_downs[1].add(default_value2); + + // Prevent from adding the default value again + first_opening = false; + } + + // Hide the second set of drop downs if "Not:" is selected + if (selection.value == 5) { + drop_downs[1].style.visibility="hidden"; + drop_downs_layer_values[1].style.visibility="hidden"; + } + } + else { + // If the user has the default value selected, hide all drop downs + var drop_downs = document.getElementsByClassName("set-operation-value"); + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="hidden"; + } + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="hidden"; + } + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "hidden"; + } +} + +function update_set_operation_selections () { + // This function is called when the shorlist is changed. + // It appropriately updates the drop down containing the list of layers + // to match the layers found in the shortlist. + + // Get the list of all layers + var layers = []; + $("#shortlist").children().each(function(index, element) { + // Get the layer name + var layer_name = $(element).data("layer"); + layers.push(layer_name); + }); + + // Get a list of all drop downs that contain layer names + var drop_downs = document.getElementsByClassName("set-operation-value"); + + // Remove all existing layer names from both dropdowns + var length = drop_downs[0].options.length; + do{ + drop_downs[0].remove(0); + length--; + } + while (length > 0); + var length = drop_downs[1].options.length; + do{ + drop_downs[1].remove(0); + length--; + } + while (length > 0); + + // Add the default values that were stripped in the last step. + var default_value = document.createElement("option"); + default_value.text = "Select Layer 1"; + default_value.value = 0; + drop_downs[0].add(default_value); + + var default_value2 = document.createElement("option"); + default_value2.text = "Select Layer 2"; + default_value2.value = 0; + drop_downs[1].add(default_value2); + + first_opening = false; + + // Add the layer names from the shortlist to the drop downs that store + // layer names. + for (var i = 0; i < drop_downs.length; i++){ + for (var j = 0; j < layers.length; j++) { + var option = document.createElement("option"); + option.text = layers[j]; + option.value = j+1; + drop_downs[i].add(option); + } + } + + // Remove all elements from drop downs holding the data values for the + // selected layers. This way there are no values presented when the user + // clicks on the set operation button to open it again. + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + + // Call the function containing onchange commands for these dropdowns. + // This way the data values are updated according the the selected layer. + update_set_operation_data_values (); +} + +function update_set_operation_data_values () { + // Define the onchange commands for the drop downs that hold layer names. + // This way the data values are updated according the the selected layer. + + // Get all drop down elements + var selected_function = document.getElementById ("set-operations-list"); + var drop_downs = document.getElementsByClassName("set-operation-value"); + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + + // The "Select Layer1" Dropdown onchange function + drop_downs[0].onchange = function(){ + // Strip current values of the data value dropdown + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + // Add the data values depending on the selected layer + var selectedIndex = drop_downs[0].selectedIndex; + var layer_name = drop_downs[0].options[selectedIndex].text; + var set_operation_data_value_select = set_operation_layer_values[0]; + create_set_operation_pick_list(set_operation_data_value_select, layer_name); + }; + + // The "Select Layer2" Dropdown onchange function + drop_downs[1].onchange = function(){ + // Strip current values of the data value dropdown + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + + // Add the data values depending on the selected layer + var selectedIndex = drop_downs[1].selectedIndex; + var layer_name = drop_downs[1].options[selectedIndex].text; + var set_operation_data_value_select = set_operation_layer_values[1]; + create_set_operation_pick_list(set_operation_data_value_select, layer_name); + }; + +} + +function create_set_operation_pick_list(value,layer_object) { + + // We must create a drop down containing the data values for the selected + // layer. + + // The Javascript "select" element that contains the data values + // is passed as "value" and the selected layer is passed as "layer_object". + + // First, figure out what kind of filter settings we take based on + // what kind of layer we are. + with_layer(layer_object, function(layer) { + + // No options available. We have to add them. + for(var i = 0; i < layer.magnitude + 1; i++) { + // Make an option for each value; + var option = document.createElement("option"); + option.value = i; + + if(colormaps[layer_object].hasOwnProperty(i)) { + // We have a real name for this value + option.text = (colormaps[layer_object][i].name); + } else { + // No name. Use the number. + option.text = i; + } + value.add(option); + + // Select the last option, so that 1 on 0/1 layers will + // be selected by default. + var last_index = value.options.length - 1; + value.selectedIndex = last_index; + } + // Now that the right controls are there, assume they have + refresh(); + }); +} + + +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" + }); + + update_set_operation_selections (); +} + +function uncheck_checkbox (checkbox_class) { + // Unchecks chekboxes after the function has been completed. + var checkboxArray = new Array (); + checkboxArray = document.getElementsByClassName(checkbox_class); + for (var i = 0; i < checkboxArray.length; i++) + { + checkboxArray[i].checked = false; + } +} + +function hide_values (set_theory_function) { + // Hides pick lists for set theory functions after function has been + // completed. + var value_type = set_theory_function + '-value'; + + var values = new Array (); + + values = document.getElementsByClassName(value_type); + + var length = values.length; + + for (var i = 0; i < length; i++) + { + values[i].style.display = 'none'; + } + refresh(); +} + +function compute_intersection (values, intersection_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the intersection utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures that intersect + var intersection_signatures = []; + + with_layers (intersection_layer_names, function (intersection_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + if (intersection_layers[0].data[hex] == values[0] && intersection_layers[1].data[hex] == values[1]){ + intersection_signatures.push(hex); + } + } + }); + + for (var i = 0; i < intersection_layer_names.length; i++){ + intersection_layer_names[i] = intersection_layer_names[i] + " [" + text[i] + "]"; + } + var intersection_function = "intersection"; + select_list (intersection_signatures, intersection_function, intersection_layer_names); + uncheck_checkbox ('intersection-checkbox'); + hide_values('intersection'); +} + +function compute_union (values, union_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the union utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var union_signatures = []; + + with_layers (union_layer_names, function (union_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Union Function + if (union_layers[0].data[hex] == values[0] || union_layers[1].data[hex] == values[1]){ + union_signatures.push(hex); + } + } + }); + + for (var i = 0; i < union_layer_names.length; i++){ + union_layer_names[i] = union_layer_names[i] + " [" + text[i] + "]"; + } + + var union_function = "union"; + select_list (union_signatures, union_function, union_layer_names); + uncheck_checkbox ('union-checkbox'); + hide_values('union'); +} + +function compute_set_difference (values, set_difference_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var set_difference_signatures = []; + + with_layers (set_difference_layer_names, function (set_difference_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Set Difference Function + if (set_difference_layers[0].data[hex] == values[0] && + set_difference_layers[1].data[hex] != values[1]){ + set_difference_signatures.push(hex); + } + } + }); + + for (var i = 0; i < set_difference_layer_names.length; i++){ + set_difference_layer_names[i] = set_difference_layer_names[i] + " [" + text[i] + "]"; + } + + var set_difference_function = "set difference"; + select_list (set_difference_signatures, set_difference_function, set_difference_layer_names); + uncheck_checkbox ('set-difference-checkbox'); + hide_values('set-difference'); +} + +function compute_symmetric_difference (values, symmetric_difference_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var symmetric_difference_signatures = []; + + with_layers (symmetric_difference_layer_names, function (symmetric_difference_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Symmetric Difference Function + if (symmetric_difference_layers[0].data[hex] == values[0] && + symmetric_difference_layers[1].data[hex] != values[1]){ + symmetric_difference_signatures.push(hex); + } + if (symmetric_difference_layers[0].data[hex] != values[0] && + symmetric_difference_layers[1].data[hex] == values[1]){ + symmetric_difference_signatures.push(hex); + } + } + }); + + for (var i = 0; i < symmetric_difference_layer_names.length; i++){ + symmetric_difference_layer_names[i] = symmetric_difference_layer_names[i] + " [" + text[i] + "]"; + } + + var symmetric_difference_function = "symmetric difference"; + select_list (symmetric_difference_signatures, symmetric_difference_function, symmetric_difference_layer_names); + uncheck_checkbox ('symmetric-difference-checkbox'); + hide_values('symmetric-difference'); +} + +function compute_absolute_complement (values, absolute_complement_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var absolute_complement_signatures = []; + + with_layers (absolute_complement_layer_names, function (absolute_complement_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Absolute Complement Function + if (absolute_complement_layers[0].data[hex] != values[0]) { + absolute_complement_signatures.push(hex); + } + } + }); + + for (var i = 0; i < absolute_complement_layer_names.length; i++){ + absolute_complement_layer_names[i] = absolute_complement_layer_names[i] + " [" + text[i] + "]"; + } + var absolute_complement_function = "absolute complement"; + select_list (absolute_complement_signatures, absolute_complement_function, absolute_complement_layer_names); + uncheck_checkbox ('absolute-complement-checkbox'); + hide_values('absolute-complement'); +} + + +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 each 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; + } + + // Do some transformations to make the displayed labels make more sense + lookup = { + n: "Number of non-empty values", + positives: "Number of ones", + inside_yes: "Ones in A", + outside_yes: "Ones in background" + } + + if(lookup[attribute]) { + // Replace a boring short name with a useful long name + attribute = lookup[attribute]; + } + + // 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_toggle_layout_ui(layout_name) { + // Returns a jQuery element to represent the layer layout the given name in + // the toggle layout panel. + + // This holds a jQuery element that's the root of the structure we're + // building. + var root = $("<div/>").addClass("layout-entry"); + root.data("layout-name", layout_name); + + // Put in the layer name in a div that makes it wrap. + root.append($("<div/>").addClass("layout-name").text(layout_name)); + + return root; +} + +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 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_set_theory_layers(function_type) { + // Returns an array of layer names that have been selected. + // This function only looks at the layers that are listed on the shortlist. + + var current_set_theory_layers = []; + + // Initialize global variables that hold the number of checkboxes selected + // for set theory functions to zero so that the new number is calculated + // each time this function is called. + + if (function_type == "intersection"){ + shortlist_intersection_num = 0; + } + + if (function_type == "union"){ + shortlist_union_num = 0; + } + + if (function_type == "set difference"){ + shortlist_set_difference_num = 0; + } + + if (function_type == "symmetric difference"){ + shortlist_symmetric_difference_num = 0; + } + + if (function_type == "absolute complement"){ + shortlist_absolute_complement_num = 0; + } + + $("#shortlist").children().each(function(index, element) { + // Go through all the shortlist entries. + + // This holds the checkbox that determines if we use this layer + // The class name depends on the function_type. + + // If intersection function look for intersection-checkbox. + if (function_type == "intersection"){ + var checkbox = $(element).find(".intersection-checkbox"); + } + + // If union function look for union-checkbox. + if (function_type == "union"){ + var checkbox = $(element).find(".union-checkbox"); + } + + // If set difference function look for set-difference-checkbox. + if (function_type == "set difference"){ + var checkbox = $(element).find(".set-difference-checkbox"); + } + + // If symmetric difference function look for + // symmetric-difference-checkbox. + if (function_type == "symmetric difference"){ + var checkbox = $(element).find(".symmetric-difference-checkbox"); + } + + if (function_type == "absolute complement"){ + var checkbox = $(element).find(".absolute-complement-checkbox"); + } + + if(checkbox.is(":checked")) { + // Put the layer in if its checkbox is checked. + + + // Get the layer name + var layer_name = $(element).data("layer"); + + // Add the layer_name to the list of current_set_theory_layers. + current_set_theory_layers.push(layer_name); + + // Add to the global "num" variables to keep track of the number + // of selected checkboxes. + + if (function_type == "intersection"){ + shortlist_intersection_num++; + } + + if (function_type == "union"){ + shortlist_union_num++; + } + + if (function_type == "set difference"){ + shortlist_set_difference_num++; + } + + if (function_type == "symmetric difference"){ + shortlist_symmetric_difference_num++; + } + + if (function_type == "absolute complement"){ + shortlist_absolute_complement_num++; + } + + } + }); + + return current_set_theory_layers; +} + + +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, function_type, layer_names) { + // 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. + + // function_type is an optional parameter. If no variable is passed for the + // function_type undefined then the value will be undefined and the + // default "selection + #" title will be assigned to the shortlist element. + // If layer_names is undefined, the "selection + #" will also apply as a + // default. However, if a value i.e. "intersection" is passed + // for function_type, the layer_names will be used along with the + // function_type to assign the correct title. + + // 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; + + // Default Values for Optional Parameters + if (function_type == undefined && layer_names == undefined){ + layer_name = "Selection " + selection_next_id; + selection_next_id++; + } + + if (function_type == "user selection"){ + var text = prompt("Please provide a label for your selection", + "Selection Label Text"); + if (text != null){ + layer_name = text; + } + if (!text) + { + return; + } + } + + // intersection for layer name + if (function_type == "intersection"){ + layer_name = "(" + layer_names[0] + " ∩ " + layer_names[1] + ")"; + } + + // union for layer name + if (function_type == "union"){ + layer_name = "(" + layer_names[0] + " U " + layer_names[1] + ")"; + } + + // set difference for layer name + if (function_type == "set difference"){ + layer_name = "(" + layer_names[0] + " \\ " + layer_names[1] + ")"; + } + + // symmetric difference for layer name + if (function_type == "symmetric difference"){ + layer_name = "(" + layer_names[0] + " ∆ " + layer_names[1] + ")"; + } + + // absolute complement for layer name + if (function_type == "absolute complement"){ + layer_name = "Not: " + "(" + layer_names[0] + ")"; + } + + // saved filter for layer name + if (function_type == "save"){ + layer_name = "(" + layer_names[0] + ")"; + } + + // 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. + + var select_function_type = "user selection"; + select_list(in_box, select_function_type); + +} + +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(results) { + + // The statistics code really sends back a dict of updated metadata for + // each layer. Copy it over. + for(var metadata in results) { + layers[layer_name][metadata] = results[metadata]; + } + + 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) { + // The statistics code really sends back a dict of updated metadata + // for each layer. Copy it over. + for(var metadata in result[layer_name]) { + layers[layer_name][metadata] = result[layer_name][metadata]; + } + } + + 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; + + // Send its error events to our error processor + worker.onerror = rpc_error; + + // 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 rpc_error(error) { + // Handle an error event from a web worker + // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.h + // tml#errorevent + + complain("Web Worker error: " + error.message); + print(error.message + "\n at" + error.filename + " line " + error.lineno + + " column " + error.column); +} + +function initialize_view(initial_zoom) { + // 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: initial_zoom, + 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 clearMap() { + +} + +function drl_values(layout_index) { + + // Download the DrL position data, and make it into a layer + $.get("drl"+ layout_index +".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"); +} + +function assignment_values (layout_index, spacing) { + // Download the signature assignments to hexagons and fill in the global + // hexagon assignment grid. + $.get("assignments" + layout_index +".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]); + + x = x * spacing; + y = y * spacing; + + + // 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]); + + x = x * spacing; + y = y * spacing; + + // 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"); +} + +// Function to create a new map based upon the the layout_name argument +// Find the index of the layout_name and pass it as the index to the +// drl_values and assignment_values functions as these files are indexed +// according to the appropriate layout +function recreate_map(layout_name, spacing) { + + var layout_index = layout_names.indexOf(layout_name); + drl_values(layout_index); + assignment_values(layout_index, spacing); + +} + +$(function() { + + // Set up the RPC system for background statistics + rpc_initialize(); + + // Set up the Google Map + initialize_view(0); + + // 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); + }); + }); + + // Temporary Inflate Button + $("#inflate").button().click(function() { + initialize_view (0); + recreate_map(current_layout_name, 2); + refresh (); + }); + + // Create Pop-Up UI for Set Operations + $("#set-operations").prepend(create_set_operation_ui ()); + + // Action handler for display of set operation pop-up + $("#set-operation").button().click(function() { + set_operation_clicks++; + if (set_operation_clicks % 2 != 0){ + show_set_operation_drop_down (); + } + else { + hide_set_operation_drop_down (); + var drop_downs = document.getElementsByClassName("set-operation-value"); + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="hidden"; + } + } + + }); + + // Coputation of Set Operations + var compute_button = document.getElementsByClassName ("compute-button"); + compute_button[0].onclick = function () { + var layer_names = []; + var layer_values = []; + var layer_values_text = []; + + var drop_down_layers = document.getElementsByClassName("set-operation-value"); + var drop_down_data_values = document.getElementsByClassName("set-operation-layer-value"); + + var function_type = document.getElementById("set-operations-list"); + var selected_function = function_type.selectedIndex; + + var selected_index = drop_down_layers[0].selectedIndex; + layer_names.push(drop_down_layers[0].options[selected_index].text); + + var selected_index = drop_down_data_values[0].selectedIndex; + layer_values.push(drop_down_data_values[0].options[selected_index].value); + layer_values_text.push(drop_down_data_values[0].options[selected_index].text); + + if (selected_function != 5) { + var selected_index = drop_down_data_values[1].selectedIndex; + layer_values.push(drop_down_data_values[1].options[selected_index].value); + layer_values_text.push(drop_down_data_values[1].options[selected_index].text); + var selected_index = drop_down_layers[1].selectedIndex; + layer_names.push(drop_down_layers[1].options[selected_index].text); + } + + + switch (selected_function) { + case 1: + compute_intersection(layer_values, layer_names, layer_values_text); + break; + case 2: + compute_union(layer_values, layer_names, layer_values_text); + break; + case 3: + compute_set_difference(layer_values, layer_names, layer_values_text); + break; + case 4: + compute_symmetric_difference(layer_values, layer_names, layer_values_text); + break; + case 5: + compute_absolute_complement(layer_values, layer_names, layer_values_text); + break + default: + complain ("Set Theory Error"); + } + }; + + // 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"); + +// Download the Matrix Names and pass it to the layout_names array + $.get("matrixnames.tab", function(tsv_data) { + // This is an array of rows, which are strings of matrix names + var parsed = $.tsv.parseRows(tsv_data); + + for(var i = 0; i < parsed.length; i++) { + // Pull out the parts of the TSV entry + var label = parsed[i][0]; + + if(label == "") { + // Skip any blank lines + continue; + } + // Add layout names to global array of names + layout_names.push(label); + + if(layout_names.length == 1) { + // This is the very first layout. Pull it up. + + // TODO: We don't go through the normal change event since we + // never change the dropdown value actually. But we duplicate + // user selection hode here. + var current_layout = "Current Layout: " + layout_names[0]; + + $("#current-layout").text(current_layout); + initialize_view (0); + recreate_map(layout_names[0], 1); + refresh (); + current_layout_name = layout_names[0]; + + } + } + }, "text"); + + $("#layout-search").select2({ + placeholder: "Select a Layout...", + 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 < layout_names.length; i++) { + // For each possible result + if(layout_names[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: layout_names[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 < layout_names.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_toggle_layout_ui(result.id); + }, + // We want our dropdown to be big enough to browse. + dropdownCssClass: "results-dropdown" + }); + + // Handle result selection + $("#layout-search").on("select2-selecting", function(event) { + // The select2 id of the thing clicked (the layout's name) is event.val + var layout_name = event.val; + + var current_layout = "Current Layout: " + layout_name; + + $("#current-layout").text(current_layout); + initialize_view (0); + recreate_map(layout_name, 1); + refresh (); + // Don't actually change the selection. + // This keeps the dropdown open when we click. + event.preventDefault(); + + current_layout_name = layout_name; + }); + + drl_values(layout_names[0]); + assignment_values (layout_names[0], 1); + current_layout_name = layout_names[0]; + +}); + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/hexagram.js~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,3848 @@ +// 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 is a list of the map-layour names mantained in order of entry +var layout_names = []; + +// 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 is a list of layer names whose intersection checkbox has been selected. +var shortlist_intersection = []; + +//This is the number of intersection checkboxes that have been selected. +var shortlist_intersection_num = 0; + +// This is a list of layer names whose union checkbox has been selected. +var shortlist_union = []; + +//This is the number of union checkboxes that have been selected. +var shortlist_union_num = 0; + +//This is a list of layer names whose set difference checkbox has been selected. +var shortlist_set_difference = []; + +// This is the number of set difference checkboxes that have been selected. +var shortlist_set_difference_num = 0; + +// This is a list of the layer names whose symmetric difference checkbox +// has been selected. +var shortlist_symmetric_difference = []; + +// This is the number of symmetric difference checkboxes that have been +// selected. +var shortlist_symmetric_difference_num = 0; + +// This is an array containing the layer whose absolute complement checkbox +// has been selected. +var shortlist_absolute_complement = []; + +// This is the number of absolute complement checkboxes that have been selected. +var shortlist_absolute_complement_num = 0; + +// Records number of set-operation clicks +var set_operation_clicks = 0; + +// Boolean stating whether this is the first time the set operation popup +// has been created so that "Select Layer" Default is added only once +var first_opening = true; + +// Boolean for Creating Layer from Filter +var created = false; + +// Stores the Name of Current Layer Displayed +var current_layout_name; + +// 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" +]; + +// This is a global variable that keeps track of the current Goolge Map zoom +// This is needed to keep viewing consistent across layouts +var global_zoom = 0; + +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(1250).fadeOut(1000); + + 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 + }; + + var check_layer_exists = layers[layer_name]; + + 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]; + + var data_val = layer.data; + 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.addClass("filter-image"); + 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); + + // Add a image for the save function + var save_filter = $("<img/>").attr("src", "save.svg"); + save_filter.addClass("save-filter"); + save_filter.attr("title", "Save Filter as Layer"); + + contents.append(filter_holder); + contents.append(save_filter); + + 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(); + } + + save_filter.show (); + + save_filter.button().click(function() { + // Configure Save Filter Buttons + + // Get selected value + var selected = filter_value.prop("selectedIndex"); + var value = filter_value.val(); + + var signatures = []; + + // Gather Tumor-ID Signatures with value and push to "signatures" + for (hex in polygons){ + if (layer.data[hex] == value){ + signatures.push(hex); + } + } + + // Create Layer + if (created == false) { + select_list (signatures, "user selection"); + created = true; + } + created = false; + }); + + + // Now that the right controls are there, assume they have + refresh(); + + }); + } else { + created = false; + // Hide the filtering settings + filter_value.hide(); + filter_threshold.hide(); + save_filter.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; +} + +// ____________________________________________________________________________ +// Replacement Set Operation Code +// ____________________________________________________________________________ +function get_set_operation_selection () { + // For the new dop-down GUI for set operation selection + // we neeed a function to determine which set operation is selected. + // This way we can display the appropriate divs. + + // Drop Down List & Index for Selected Element + var drop_down = document.getElementById("set-operations-list"); + var index = drop_down.selectedIndex; + var selection = drop_down.options[index]; + + return selection; +} + +function show_set_operation_drop_down () { + // Show Set Operation Drop Down Menu + document.getElementsByClassName("set-operation-col")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel")[0].style.visibility="visible"; + document.getElementById("set-operations").style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="visible"; + document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="visible"; + +} + +function hide_set_operation_drop_down () { + // Hide Set Operation Drop Down Menu + document.getElementsByClassName("set-operation-col")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel")[0].style.visibility="hidden"; + document.getElementById("set-operations").style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="hidden"; + document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="hidden"; + + // Hide the Data Values for the Selected Layers + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="hidden"; + } + + // Hide the Compute Button + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "hidden"; + + // Set the "Select Layer" drop down to the default value + var list = document.getElementById("set-operations-list"); + list.selectedIndex = 0; + + var list_value = document.getElementsByClassName("set-operation-value"); + list_value[0].selectedIndex = 0; + list_value[1].selectedIndex = 0; + + // Remove all elements from drop downs holding the data values for the + // selected layers. This way there are no values presented when the user + // clicks on the set operation button to open it again. + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + +} + +function create_set_operation_ui () { + // Returns a Jquery element that is then prepended to the existing + // set theory drop-down menu + + // This holds the root element for this set operation UI + var root = $("<div/>").addClass("set-operation-entry"); + + // Add Drop Downs to hold the selected layers and and selected data values + var set_theory_value1 = $("<select/>").addClass("set-operation-value"); + var set_theory_layer_value1 = $("<select/>").addClass("set-operation-layer-value"); + var set_theory_value2 = $("<select/>").addClass("set-operation-value"); + var set_theory_layer_value2 = $("<select/>").addClass("set-operation-layer-value"); + + var compute_button = $("<input/>").attr("type", "button"); + compute_button.addClass ("compute-button"); + + // Append to Root + root.append (set_theory_value1); + root.append (set_theory_layer_value1); + root.append (set_theory_value2); + root.append (set_theory_layer_value2); + root.append (compute_button); + + return root; +} + +function update_set_operation_drop_down () { + // This is the onchange command for the drop down displaying the + // different set operation functions. It is called whenever the user changes + // the selected set operation. + + // Get the value of the set operation selection made by the user. + var selection = get_set_operation_selection(); + var value = selection.value; + // Check if the selectin value is that of one of set operation functions + if (selection.value == 1 || selection.value == 2 + || selection.value == 3 || selection.value == 4 + || selection.value == 5){ + // Make the drop downs that hold layer names and data values visible + var drop_downs = document.getElementsByClassName("set-operation-value"); + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="visible"; + } + + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="visible"; + } + + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "visible"; + compute_button[0].value = "Compute Set Operation"; + + if (first_opening == true) { + // Set the default value for the drop down, holding the selected layers + var default_value = document.createElement("option"); + default_value.text = "Select Layer 1"; + default_value.value = 0; + drop_downs[0].add(default_value); + + var default_value2 = document.createElement("option"); + default_value2.text = "Select Layer 2"; + default_value2.value = 0; + drop_downs[1].add(default_value2); + + // Prevent from adding the default value again + first_opening = false; + } + + // Hide the second set of drop downs if "Not:" is selected + if (selection.value == 5) { + drop_downs[1].style.visibility="hidden"; + drop_downs_layer_values[1].style.visibility="hidden"; + } + } + else { + // If the user has the default value selected, hide all drop downs + var drop_downs = document.getElementsByClassName("set-operation-value"); + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="hidden"; + } + var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); + for (var i = 0; i < drop_downs_layer_values.length; i++) { + drop_downs_layer_values[i].style.visibility="hidden"; + } + var compute_button = document.getElementsByClassName("compute-button"); + compute_button[0].style.visibility = "hidden"; + } +} + +function update_set_operation_selections () { + // This function is called when the shorlist is changed. + // It appropriately updates the drop down containing the list of layers + // to match the layers found in the shortlist. + + // Get the list of all layers + var layers = []; + $("#shortlist").children().each(function(index, element) { + // Get the layer name + var layer_name = $(element).data("layer"); + layers.push(layer_name); + }); + + // Get a list of all drop downs that contain layer names + var drop_downs = document.getElementsByClassName("set-operation-value"); + + // Remove all existing layer names from both dropdowns + var length = drop_downs[0].options.length; + do{ + drop_downs[0].remove(0); + length--; + } + while (length > 0); + var length = drop_downs[1].options.length; + do{ + drop_downs[1].remove(0); + length--; + } + while (length > 0); + + // Add the default values that were stripped in the last step. + var default_value = document.createElement("option"); + default_value.text = "Select Layer 1"; + default_value.value = 0; + drop_downs[0].add(default_value); + + var default_value2 = document.createElement("option"); + default_value2.text = "Select Layer 2"; + default_value2.value = 0; + drop_downs[1].add(default_value2); + + first_opening = false; + + // Add the layer names from the shortlist to the drop downs that store + // layer names. + for (var i = 0; i < drop_downs.length; i++){ + for (var j = 0; j < layers.length; j++) { + var option = document.createElement("option"); + option.text = layers[j]; + option.value = j+1; + drop_downs[i].add(option); + } + } + + // Remove all elements from drop downs holding the data values for the + // selected layers. This way there are no values presented when the user + // clicks on the set operation button to open it again. + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + + // Call the function containing onchange commands for these dropdowns. + // This way the data values are updated according the the selected layer. + update_set_operation_data_values (); +} + +function update_set_operation_data_values () { + // Define the onchange commands for the drop downs that hold layer names. + // This way the data values are updated according the the selected layer. + + // Get all drop down elements + var selected_function = document.getElementById ("set-operations-list"); + var drop_downs = document.getElementsByClassName("set-operation-value"); + var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); + + // The "Select Layer1" Dropdown onchange function + drop_downs[0].onchange = function(){ + // Strip current values of the data value dropdown + var length = set_operation_layer_values[0].options.length; + do{ + set_operation_layer_values[0].remove(0); + length--; + } + while (length > 0); + + // Add the data values depending on the selected layer + var selectedIndex = drop_downs[0].selectedIndex; + var layer_name = drop_downs[0].options[selectedIndex].text; + var set_operation_data_value_select = set_operation_layer_values[0]; + create_set_operation_pick_list(set_operation_data_value_select, layer_name); + }; + + // The "Select Layer2" Dropdown onchange function + drop_downs[1].onchange = function(){ + // Strip current values of the data value dropdown + var length = set_operation_layer_values[1].options.length; + do{ + set_operation_layer_values[1].remove(0); + length--; + } + while (length > 0); + + // Add the data values depending on the selected layer + var selectedIndex = drop_downs[1].selectedIndex; + var layer_name = drop_downs[1].options[selectedIndex].text; + var set_operation_data_value_select = set_operation_layer_values[1]; + create_set_operation_pick_list(set_operation_data_value_select, layer_name); + }; + +} + +function create_set_operation_pick_list(value,layer_object) { + + // We must create a drop down containing the data values for the selected + // layer. + + // The Javascript "select" element that contains the data values + // is passed as "value" and the selected layer is passed as "layer_object". + + // First, figure out what kind of filter settings we take based on + // what kind of layer we are. + with_layer(layer_object, function(layer) { + + // No options available. We have to add them. + for(var i = 0; i < layer.magnitude + 1; i++) { + // Make an option for each value; + var option = document.createElement("option"); + option.value = i; + + if(colormaps[layer_object].hasOwnProperty(i)) { + // We have a real name for this value + option.text = (colormaps[layer_object][i].name); + } else { + // No name. Use the number. + option.text = i; + } + value.add(option); + + // Select the last option, so that 1 on 0/1 layers will + // be selected by default. + var last_index = value.options.length - 1; + value.selectedIndex = last_index; + } + // Now that the right controls are there, assume they have + refresh(); + }); +} + + +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" + }); + + update_set_operation_selections (); +} + +function uncheck_checkbox (checkbox_class) { + // Unchecks chekboxes after the function has been completed. + var checkboxArray = new Array (); + checkboxArray = document.getElementsByClassName(checkbox_class); + for (var i = 0; i < checkboxArray.length; i++) + { + checkboxArray[i].checked = false; + } +} + +function hide_values (set_theory_function) { + // Hides pick lists for set theory functions after function has been + // completed. + var value_type = set_theory_function + '-value'; + + var values = new Array (); + + values = document.getElementsByClassName(value_type); + + var length = values.length; + + for (var i = 0; i < length; i++) + { + values[i].style.display = 'none'; + } + refresh(); +} + +function compute_intersection (values, intersection_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the intersection utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures that intersect + var intersection_signatures = []; + + with_layers (intersection_layer_names, function (intersection_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + if (intersection_layers[0].data[hex] == values[0] && intersection_layers[1].data[hex] == values[1]){ + intersection_signatures.push(hex); + } + } + }); + + for (var i = 0; i < intersection_layer_names.length; i++){ + intersection_layer_names[i] = intersection_layer_names[i] + " [" + text[i] + "]"; + } + var intersection_function = "intersection"; + select_list (intersection_signatures, intersection_function, intersection_layer_names); + uncheck_checkbox ('intersection-checkbox'); + hide_values('intersection'); +} + +function compute_union (values, union_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the union utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var union_signatures = []; + + with_layers (union_layer_names, function (union_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Union Function + if (union_layers[0].data[hex] == values[0] || union_layers[1].data[hex] == values[1]){ + union_signatures.push(hex); + } + } + }); + + for (var i = 0; i < union_layer_names.length; i++){ + union_layer_names[i] = union_layer_names[i] + " [" + text[i] + "]"; + } + + var union_function = "union"; + select_list (union_signatures, union_function, union_layer_names); + uncheck_checkbox ('union-checkbox'); + hide_values('union'); +} + +function compute_set_difference (values, set_difference_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var set_difference_signatures = []; + + with_layers (set_difference_layer_names, function (set_difference_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Set Difference Function + if (set_difference_layers[0].data[hex] == values[0] && + set_difference_layers[1].data[hex] != values[1]){ + set_difference_signatures.push(hex); + } + } + }); + + for (var i = 0; i < set_difference_layer_names.length; i++){ + set_difference_layer_names[i] = set_difference_layer_names[i] + " [" + text[i] + "]"; + } + + var set_difference_function = "set difference"; + select_list (set_difference_signatures, set_difference_function, set_difference_layer_names); + uncheck_checkbox ('set-difference-checkbox'); + hide_values('set-difference'); +} + +function compute_symmetric_difference (values, symmetric_difference_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var symmetric_difference_signatures = []; + + with_layers (symmetric_difference_layer_names, function (symmetric_difference_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Symmetric Difference Function + if (symmetric_difference_layers[0].data[hex] == values[0] && + symmetric_difference_layers[1].data[hex] != values[1]){ + symmetric_difference_signatures.push(hex); + } + if (symmetric_difference_layers[0].data[hex] != values[0] && + symmetric_difference_layers[1].data[hex] == values[1]){ + symmetric_difference_signatures.push(hex); + } + } + }); + + for (var i = 0; i < symmetric_difference_layer_names.length; i++){ + symmetric_difference_layer_names[i] = symmetric_difference_layer_names[i] + " [" + text[i] + "]"; + } + + var symmetric_difference_function = "symmetric difference"; + select_list (symmetric_difference_signatures, symmetric_difference_function, symmetric_difference_layer_names); + uncheck_checkbox ('symmetric-difference-checkbox'); + hide_values('symmetric-difference'); +} + +function compute_absolute_complement (values, absolute_complement_layer_names, text) { + // A function that will take a list of layer names + // that have been selected for the set difference utility. + // Fetches the respective layers and list of tumor ids. + // Then compares data elements of the same tumor id + // between both layers. Adds these hexes to a new layer + // for visualization + + //Array of signatures + var absolute_complement_signatures = []; + + with_layers (absolute_complement_layer_names, function (absolute_complement_layers) { + + // Gather Tumor-ID Signatures. + for (hex in polygons) + { + // Absolute Complement Function + if (absolute_complement_layers[0].data[hex] != values[0]) { + absolute_complement_signatures.push(hex); + } + } + }); + + for (var i = 0; i < absolute_complement_layer_names.length; i++){ + absolute_complement_layer_names[i] = absolute_complement_layer_names[i] + " [" + text[i] + "]"; + } + var absolute_complement_function = "absolute complement"; + select_list (absolute_complement_signatures, absolute_complement_function, absolute_complement_layer_names); + uncheck_checkbox ('absolute-complement-checkbox'); + hide_values('absolute-complement'); +} + + +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 each 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; + } + + // Do some transformations to make the displayed labels make more sense + lookup = { + n: "Number of non-empty values", + positives: "Number of ones", + inside_yes: "Ones in A", + outside_yes: "Ones in background" + } + + if(lookup[attribute]) { + // Replace a boring short name with a useful long name + attribute = lookup[attribute]; + } + + // 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_toggle_layout_ui(layout_name) { + // Returns a jQuery element to represent the layer layout the given name in + // the toggle layout panel. + + // This holds a jQuery element that's the root of the structure we're + // building. + var root = $("<div/>").addClass("layout-entry"); + root.data("layout-name", layout_name); + + // Put in the layer name in a div that makes it wrap. + root.append($("<div/>").addClass("layout-name").text(layout_name)); + + return root; +} + +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 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_set_theory_layers(function_type) { + // Returns an array of layer names that have been selected. + // This function only looks at the layers that are listed on the shortlist. + + var current_set_theory_layers = []; + + // Initialize global variables that hold the number of checkboxes selected + // for set theory functions to zero so that the new number is calculated + // each time this function is called. + + if (function_type == "intersection"){ + shortlist_intersection_num = 0; + } + + if (function_type == "union"){ + shortlist_union_num = 0; + } + + if (function_type == "set difference"){ + shortlist_set_difference_num = 0; + } + + if (function_type == "symmetric difference"){ + shortlist_symmetric_difference_num = 0; + } + + if (function_type == "absolute complement"){ + shortlist_absolute_complement_num = 0; + } + + $("#shortlist").children().each(function(index, element) { + // Go through all the shortlist entries. + + // This holds the checkbox that determines if we use this layer + // The class name depends on the function_type. + + // If intersection function look for intersection-checkbox. + if (function_type == "intersection"){ + var checkbox = $(element).find(".intersection-checkbox"); + } + + // If union function look for union-checkbox. + if (function_type == "union"){ + var checkbox = $(element).find(".union-checkbox"); + } + + // If set difference function look for set-difference-checkbox. + if (function_type == "set difference"){ + var checkbox = $(element).find(".set-difference-checkbox"); + } + + // If symmetric difference function look for + // symmetric-difference-checkbox. + if (function_type == "symmetric difference"){ + var checkbox = $(element).find(".symmetric-difference-checkbox"); + } + + if (function_type == "absolute complement"){ + var checkbox = $(element).find(".absolute-complement-checkbox"); + } + + if(checkbox.is(":checked")) { + // Put the layer in if its checkbox is checked. + + + // Get the layer name + var layer_name = $(element).data("layer"); + + // Add the layer_name to the list of current_set_theory_layers. + current_set_theory_layers.push(layer_name); + + // Add to the global "num" variables to keep track of the number + // of selected checkboxes. + + if (function_type == "intersection"){ + shortlist_intersection_num++; + } + + if (function_type == "union"){ + shortlist_union_num++; + } + + if (function_type == "set difference"){ + shortlist_set_difference_num++; + } + + if (function_type == "symmetric difference"){ + shortlist_symmetric_difference_num++; + } + + if (function_type == "absolute complement"){ + shortlist_absolute_complement_num++; + } + + } + }); + + return current_set_theory_layers; +} + + +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, function_type, layer_names) { + // 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. + + // function_type is an optional parameter. If no variable is passed for the + // function_type undefined then the value will be undefined and the + // default "selection + #" title will be assigned to the shortlist element. + // If layer_names is undefined, the "selection + #" will also apply as a + // default. However, if a value i.e. "intersection" is passed + // for function_type, the layer_names will be used along with the + // function_type to assign the correct title. + + // 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; + + // Default Values for Optional Parameters + if (function_type == undefined && layer_names == undefined){ + layer_name = "Selection " + selection_next_id; + selection_next_id++; + } + + if (function_type == "user selection"){ + var text = prompt("Please provide a label for your selection", + "Selection Label Text"); + if (text != null){ + layer_name = text; + } + if (!text) + { + return; + } + } + + // intersection for layer name + if (function_type == "intersection"){ + layer_name = "(" + layer_names[0] + " ∩ " + layer_names[1] + ")"; + } + + // union for layer name + if (function_type == "union"){ + layer_name = "(" + layer_names[0] + " U " + layer_names[1] + ")"; + } + + // set difference for layer name + if (function_type == "set difference"){ + layer_name = "(" + layer_names[0] + " \\ " + layer_names[1] + ")"; + } + + // symmetric difference for layer name + if (function_type == "symmetric difference"){ + layer_name = "(" + layer_names[0] + " ∆ " + layer_names[1] + ")"; + } + + // absolute complement for layer name + if (function_type == "absolute complement"){ + layer_name = "Not: " + "(" + layer_names[0] + ")"; + } + + // saved filter for layer name + if (function_type == "save"){ + layer_name = "(" + layer_names[0] + ")"; + } + + // 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. + + var select_function_type = "user selection"; + select_list(in_box, select_function_type); + +} + +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(results) { + + // The statistics code really sends back a dict of updated metadata for + // each layer. Copy it over. + for(var metadata in results) { + layers[layer_name][metadata] = results[metadata]; + } + + 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) { + // The statistics code really sends back a dict of updated metadata + // for each layer. Copy it over. + for(var metadata in result[layer_name]) { + layers[layer_name][metadata] = result[layer_name][metadata]; + } + } + + 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; + + // Send its error events to our error processor + worker.onerror = rpc_error; + + // 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 rpc_error(error) { + // Handle an error event from a web worker + // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.h + // tml#errorevent + + complain("Web Worker error: " + error.message); + print(error.message + "\n at" + error.filename + " line " + error.lineno + + " column " + error.column); +} + +function initialize_view(initial_zoom) { + // 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: initial_zoom, + 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 clearMap() { + +} + +function drl_values(layout_index) { + + // Download the DrL position data, and make it into a layer + $.get("drl"+ layout_index +".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"); +} + +function assignment_values (layout_index, spacing) { + // Download the signature assignments to hexagons and fill in the global + // hexagon assignment grid. + $.get("assignments" + layout_index +".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]); + + x = x * spacing; + y = y * spacing; + + + // 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]); + + x = x * spacing; + y = y * spacing; + + // 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"); +} + +// Function to create a new map based upon the the layout_name argument +// Find the index of the layout_name and pass it as the index to the +// drl_values and assignment_values functions as these files are indexed +// according to the appropriate layout +function recreate_map(layout_name, spacing) { + + var layout_index = layout_names.indexOf(layout_name); + drl_values(layout_index); + assignment_values(layout_index, spacing); + +} + +$(function() { + + // Set up the RPC system for background statistics + rpc_initialize(); + + // Set up the Google Map + initialize_view(0); + + // 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); + }); + }); + + // Temporary Inflate Button + $("#inflate").button().click(function() { + initialize_view (0); + recreate_map(current_layout_name, 2); + refresh (); + }); + + // Create Pop-Up UI for Set Operations + $("#set-operations").prepend(create_set_operation_ui ()); + + // Action handler for display of set operation pop-up + $("#set-operation").button().click(function() { + set_operation_clicks++; + if (set_operation_clicks % 2 != 0){ + show_set_operation_drop_down (); + } + else { + hide_set_operation_drop_down (); + var drop_downs = document.getElementsByClassName("set-operation-value"); + for (var i = 0; i < drop_downs.length; i++) { + drop_downs[i].style.visibility="hidden"; + } + } + + }); + + // Coputation of Set Operations + var compute_button = document.getElementsByClassName ("compute-button"); + compute_button[0].onclick = function () { + var layer_names = []; + var layer_values = []; + var layer_values_text = []; + + var drop_down_layers = document.getElementsByClassName("set-operation-value"); + var drop_down_data_values = document.getElementsByClassName("set-operation-layer-value"); + + var function_type = document.getElementById("set-operations-list"); + var selected_function = function_type.selectedIndex; + + var selected_index = drop_down_layers[0].selectedIndex; + layer_names.push(drop_down_layers[0].options[selected_index].text); + + var selected_index = drop_down_data_values[0].selectedIndex; + layer_values.push(drop_down_data_values[0].options[selected_index].value); + layer_values_text.push(drop_down_data_values[0].options[selected_index].text); + + if (selected_function != 5) { + var selected_index = drop_down_data_values[1].selectedIndex; + layer_values.push(drop_down_data_values[1].options[selected_index].value); + layer_values_text.push(drop_down_data_values[1].options[selected_index].text); + var selected_index = drop_down_layers[1].selectedIndex; + layer_names.push(drop_down_layers[1].options[selected_index].text); + } + + + switch (selected_function) { + case 1: + compute_intersection(layer_values, layer_names, layer_values_text); + break; + case 2: + compute_union(layer_values, layer_names, layer_values_text); + break; + case 3: + compute_set_difference(layer_values, layer_names, layer_values_text); + break; + case 4: + compute_symmetric_difference(layer_values, layer_names, layer_values_text); + break; + case 5: + compute_absolute_complement(layer_values, layer_names, layer_values_text); + break + default: + complain ("Set Theory Error"); + } + }; + + // 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"); + +// Download the Matrix Names and pass it to the layout_names array + $.get("matrixnames.tab", function(tsv_data) { + // This is an array of rows, which are strings of matrix names + var parsed = $.tsv.parseRows(tsv_data); + + for(var i = 0; i < parsed.length; i++) { + // Pull out the parts of the TSV entry + var label = parsed[i][0]; + + if(label == "") { + // Skip any blank lines + continue; + } + // Add layout names to global array of names + layout_names.push(label); + + if(layout_names.length == 1) { + // This is the very first layout. Pull it up. + + // TODO: We don't go through the normal change event since we + // never change the dropdown value actually. But we duplicate + // user selection hode here. + var current_layout = "Current Layout: " + layout_names[0]; + + $("#current-layout").text(current_layout); + initialize_view (0); + recreate_map(layout_names[0], 1); + refresh (); + current_layout_name = layout_names[0]; + + } + } + }, "text"); + + $("#layout-search").select2({ + placeholder: "Select a Layout...", + 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 < layout_names.length; i++) { + // For each possible result + if(layout_names[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: layout_names[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 < layout_names.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_toggle_layout_ui(result.id); + }, + // We want our dropdown to be big enough to browse. + dropdownCssClass: "results-dropdown" + }); + + // Handle result selection + $("#layout-search").on("select2-selecting", function(event) { + // The select2 id of the thing clicked (the layout's name) is event.val + var layout_name = event.val; + + var current_layout = "Current Layout: " + layout_name; + + document.getElementById('current-layout').innerHTML=current_layout; + initialize_view (0); + recreate_map(layout_name, 1); + refresh (); + // Don't actually change the selection. + // This keeps the dropdown open when we click. + event.preventDefault(); + + current_layout_name = layout_name; + }); + + drl_values(layout_names[0]); + assignment_values (layout_names[0], 1); + current_layout_name = layout_names[0]; + +}); + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/hexagram.py Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,1219 @@ +#!/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 layout 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, multiprocessing, traceback, numpy +import scipy.stats, scipy.linalg +import os.path +import tsv + +# Global variable to hold opened matrices files +matrices = []; + + +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("similarity", type=str, nargs='+', + help="the unopened files of similarity matrices") + parser.add_argument("--names", type=str, action="append", default=[], + help="the unopened files of similarity matrices") + 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("--truncation_edges", type=int, default=10, + help="number of edges for DrL truncate to pass per node") + parser.add_argument("--no-stats", dest="stats", action="store_false", + default=True, + help="disable cluster-finding statistics") + parser.add_argument("--include-singletons", dest="singletons", + action="store_true", default=False, + help="add self-edges to retain unconnected points") + + 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 with 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 open_matrices(names): + """ + The argument parser now take multiple similarity matrices as input and + saves their file name as strings. We want to store the names of these + strings for display later in hexagram.js in order to allow the user to + navigate and know what type of visualization map they are looking at - + gene expression, copy number, etc. + + Since, the parser no longer opens the files automatically we must, do it + in this function. + """ + + # For each file name, open the file and add it to the matrices list + # 'r' is the argument stating that the file will be read-only + for similarity_filename in names: + print "Opening Matrices..." + matrix_file = tsv.TsvReader(open(similarity_filename, "r")) + matrices.append(matrix_file) + +def compute_beta (coords, matrix, axis, index, options): + """ + Compute and return a beta matrix from coords * matrix. + Then print the matrix to a file to be read on clientside. + """ + beta = coords * matrix + return beta + # Must add writing function + +def drl_similarity_functions(matrix, index, options): + """ + Performs all the functions needed to format a similarity matrix into a + tsv format whereby the DrL can take the values. Then all of the DrL + functions are performed on the similarity matrix. + + Options is passed to access options.singletons and other required apsects + of the parsed args. + """ + + # Work in a temporary directory + # If not available, create the 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. + # index added to extension in order to keep track of + # respective layouts + drl_basename = os.path.join(drl_directory, "layout" + str(index)) + + # 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 = matrix + + # This holds a writer for the sim file + sim_writer = tsv.TsvWriter(open(drl_basename + ".sim", "w")) + + print "Regularizing similarity matrix..." + sys.stdout.flush() + + # This holds a list of all unique signature names in the similarity matrix. + # We can use it to add edges to keep singletons. + signatures = set() + + print "Reach for parts in sim_reader" + for parts in sim_reader: + # Keep the signature names used + signatures.add(parts[0]) + signatures.add(parts[1]) + + # Save the line to the regularized file + sim_writer.list_line(parts) + + if options.singletons: + # Now add a self-edge on every node, so we don't drop nodes with no + # other strictly positive edges + for signature in signatures: + sim_writer.line(signature, signature, 1) + + 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", "-t", str(options.truncation_edges), + 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. It is + # also our official collection of node names that made it through DrL, and + # therefore need their score data sent to the client. + 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. + + # index added to drl.tab extension in order to keep track of + # respective drl.tabs + coord_writer = tsv.TsvWriter(open( + os.path.join(options.directory, "drl" + str(index) + ".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() + + # Delete our temporary directory. + shutil.rmtree(drl_directory) + + # Return nodes dict back to main method for further processes + return nodes + +def compute_hexagram_assignments (nodes, index, options): + """ + Now that we are taking multiple similarity matrices as inputs, we must + compute hexagram assignments for each similarity matrix. These assignments + are based up on the nodes ouput provided by the DrL function. + + Index relates each matrix name with its drl output, nodes, assignments, etc. + Options contains the parsed arguments that are present in the main method. + """ + # 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. + global hexagons + 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"+ str(index) + ".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() + + # Hand placement_badness dict to main method so that it can be used else + # where. + return placement_badnesses + +def write_matrix_names (options): + """ + Write the names of the similarity matrices so that hexagram.js can + process the names and create the toggle layout GUI. + We pass options to access the parsed args and thus the matrix names. + """ + name_writer = tsv.TsvWriter(open(os.path.join(options.directory, + "matrixnames.tab"), "w")) + for i in options.names: + name_writer.line(i) + + name_writer.close() + +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 + + print "Created Options" + + # 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) + + print "Writing matrix names..." + # We must write the file names for hexagram.js to access. + write_matrix_names(options) + + print "About to open matrices..." + + # We have file names stored in options.similarities + # We must open the files and store them in matrices list for access + open_matrices(options.similarity) + + print "Opened matrices..." + + # The nodes list stores the list of nodes for each matrix + # We must keep track of each set of nodes + nodes_multiple = [] + + print "Created nodes_multiple list..." + + # Index for drl.tab and drl.layout file naming. With indexes we can match + # file names, to matrices, to drl output files. + for index, i in enumerate (matrices): + nodes_multiple.append (drl_similarity_functions(i, index, options)) + + # Compute Hexagam Assignments for each similarity matrix's drl output, + # which is found in nodes_multiple. + + # placement_badnesses_multiple list is required to store the placement + # badness dicts that are returned by the compute_hexagram_assignments + # function. + placement_badnesses_multiple = [] + for index, i in enumerate (nodes_multiple): + placement_badnesses_multiple.append (compute_hexagram_assignments (i, index, options)) + + # 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] + + if signature_name not in nodes_multiple[0]: + # This signature wasn't in our DrL output. Don't bother + # putting its layer data in our visualization. This saves + # space and makes the client-side layer counts accurate for + # the data actually displayable. + continue + + # 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_multiple[0] + + # 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() + + # Sahil will implement linear regression code here + + # We must create a m * n matrix of samples * genes + # In order to create this matrix we first must know the number of hexes + # and mantain them in a certain order. The order is important so that + # we populate the matrix with the data values in the proper row (sample). + + # 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", + "set.svg", + "save.svg", + "inflate.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) + + 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-6ae12361157c/hexagram/hexagram.py~ Tue Oct 22 14:17:59 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] = "" + continue + + # 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> + # 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-6ae12361157c/hexagram/hexagram.xml Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<tool id="hexagram" name="Hexagram Visualization" version="0.2"> + <description>Interactive hex grid clustering visualization</description> + <requirements> + <!-- + Go get the drl-graph-layout package as defined in + tool_dependencies.xml + --> + <requirement type="package" version="1.1">drl-graph-layout</requirement> + <!-- + And go get some Python modules that aren't standard. + --> + <requirement type="python-module">numpy</requirement> + <requirement type="python-module">scipy</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 + #for $i, $s in enumerate( $similarity ) + "${s.similarity_matrix.file_name}" + #end for + #for $i, $s in enumerate ($similarity) + --names "${s.similarity_matrix.name}" + #end for + #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" + --truncation_edges $edges + #if $singletons + --include-singletons + #end if + #if $nostats + --no-stats + #end if + </command> + <inputs> + <repeat name="similarity" title="Similarity Matrices"> + <param name="similarity_matrix" type="data" format="tabular" + label="Similarity matrix of signatures to visualize"/> + </repeat> + <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="edges" type="integer" value="10" + label="Number of edges to use per node"/> + <param name="query" type="text" + label="Name of query signature" + help="A signature name, or empty for no query"/> + <param name="singletons" type="boolean" + label="Keep unconnected singleton signatures"/> + <param name="nostats" type="boolean" + label="Skip calculation of heatmap clumpiness statistics"/> + </inputs> + <outputs> + <data name="output" label="Hexagram Visualization($edges edges)" + 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> + <help> + +Hexagram Visualization +====================== + +This tool produces a "hexagram visualization": an interactive Google Maps-based two-dimensional layout of the similarity matrix data, on a hexagonal grid, with the score matrix data represented as different available colorings of the hexagons. This visualization is an HTML web page with associated support files, and appears as the tool's output in your Galaxy history; to open it, click on the eyeball icon. + +If, instead of a rich interactive Web application, you get a boring white page with some text on it, you probably have tool output sanitization on. Turn sanitization off in your universe_wsgi.ini, or download the visualization, unzip it, and open the HTML file it contains. If you want to use a downloaded visualization, you will need to open it in a browser other than Chrome (which, for security reasons, does not let local Web pages access local files programmatically). + +Input Format +------------ + +The tool takes three types of input files: + +Similarity Matrices ++++++++++++++++++++ + +The only required input file is at least one *similarity matrix*, which contains similarity information over a set of "samples" or "signatures". This file is a sparse matrix represented as three tab-delimited columns; the first two columns of each row contain the names of two signatures, and the last column contains a nonzero, non-negative floating-point "similarity" between them. No headers are used. Self-edges are permitted, and self-edges with a similarity of 1 will be added to every node if "Keep unconnected singleton signatures" is checked. The input similarity matrix need not describe a similarity graph that is connected, and similarity need not be transitive in any way. + +Score Matrices +++++++++++++++ + +You almost certainly want to run the tool with one or more *score matrices*, which specify some attribute data to overlay on the signature hexes once they are arranged. Any number of score matrices can be specified. Each score matrix is a TSV file; the first column contains sample names, and any other columns contain the values of attributes for those samples. Score matrices have a header line, which specifies some (unused) name for the column of sample names, and the name of the attribute described by each column of attribute data. + +Attribute data may be continuous (floating-point numbers), categorical (non-negative integers), or binary (0 or 1). The visualizer assumes that each column is the most restrictive type that will fit the data given; a column with all 0.0s and 1.0s will be assumed to be binary, even if the user intended it to represent a continuous value. Categories in categorical columns are assumed to be numbered from 0, even if no 0s appear in the column. + +Colormap Configuration File ++++++++++++++++++++++++++++ + +If you have categorical data to display, you probably want to write a *colormap configuration file* for it. Categorical data is displayed in the visualizer with one color per category; if you want to specify particular colors for each category, or names for each category (instead of the default labels of 0 through n), you need a colormap configuration file. Each line in this file should begin with the name of a categorical attribute, followed by, for each category used in that attribute, the category number, the desired name for the category, and a CSS color (like "white" or "#FFFFFF" or "rgb(255,255,255)) to use to represent that category. Fields on each line are separated by tab characters. + + </help> +</tool> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/hexagram.xml~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<tool id="hexagram" name="Hexagram Visualization" version="0.1"> + <description>Interactive hex grid clustering visualization</description> + <requirements> + <!-- + Go get the drl-graph-layout package as defined in + tool_dependencies.xml + --> + <requirement type="package" version="1.1">drl-graph-layout</requirement> + <!-- + And go get some Python modules that aren't standard. + --> + <requirement type="python-module">numpy</requirement> + <requirement type="python-module">scipy</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 + #for $i, $s in enumerate( $similarity ) + "${s.similarity_matrix.file_name}" + #end for + #for $i, $s in enumerate ($similarity) + --names "${s.similarity_matrix.name}" + #end for + #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" + --truncation_edges $edges + #if $singletons + --include-singletons + #end if + #if $nostats + --no-stats + #end if + </command> + <inputs> + <repeat name="similarity" title="Similarity Matrices"> + <param name="similarity_matrix" type="data" format="tabular" + label="Similarity matrix of signatures to visualize"/> + </repeat> + <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="edges" type="integer" value="10" + label="Number of edges to use per node"/> + <param name="query" type="text" + label="Name of query signature" + help="A signature name, or empty for no query"/> + <param name="singletons" type="boolean" + label="Keep unconnected singleton signatures"/> + <param name="nostats" type="boolean" + label="Skip calculation of heatmap clumpiness statistics"/> + </inputs> + <outputs> + <data name="output" label="Hexagram Visualization($edges edges)" + 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> + <help> + +Hexagram Visualization +====================== + +This tool produces a "hexagram visualization": an interactive Google Maps-based two-dimensional layout of the similarity matrix data, on a hexagonal grid, with the score matrix data represented as different available colorings of the hexagons. This visualization is an HTML web page with associated support files, and appears as the tool's output in your Galaxy history; to open it, click on the eyeball icon. + +If, instead of a rich interactive Web application, you get a boring white page with some text on it, you probably have tool output sanitization on. Turn sanitization off in your universe_wsgi.ini, or download the visualization, unzip it, and open the HTML file it contains. If you want to use a downloaded visualization, you will need to open it in a browser other than Chrome (which, for security reasons, does not let local Web pages access local files programmatically). + +Input Format +------------ + +The tool takes three types of input files: + +Similarity Matrix ++++++++++++++++++ + +The only required input file is a *similarity matrix*, which contains similarity information over a set of "samples" or "signatures". This file is a sparse matrix represented as three tab-delimited columns; the first two columns of each row contain the names of two signatures, and the last column contains a nonzero, non-negative floating-point "similarity" between them. No headers are used. Self-edges are permitted, and self-edges with a similarity of 1 will be added to every node if "Keep unconnected singleton signatures" is checked. The input similarity matrix need not describe a similarity graph that is connected, and similarity need not be transitive in any way. + +Score Matrices +++++++++++++++ + +You almost certainly want to run the tool with one or more *score matrices*, which specify some attribute data to overlay on the signature hexes once they are arranged. Any number of score matrices can be specified. Each score matrix is a TSV file; the first column contains sample names, and any other columns contain the values of attributes for those samples. Score matrices have a header line, which specifies some (unused) name for the column of sample names, and the name of the attribute described by each column of attribute data. + +Attribute data may be continuous (floating-point numbers), categorical (non-negative integers), or binary (0 or 1). The visualizer assumes that each column is the most restrictive type that will fit the data given; a column with all 0.0s and 1.0s will be assumed to be binary, even if the user intended it to represent a continuous value. Categories in categorical columns are assumed to be numbered from 0, even if no 0s appear in the column. + +Colormap Configuration File ++++++++++++++++++++++++++++ + +If you have categorical data to display, you probably want to write a *colormap configuration file* for it. Categorical data is displayed in the visualizer with one color per category; if you want to specify particular colors for each category, or names for each category (instead of the default labels of 0 through n), you need a colormap configuration file. Each line in this file should begin with the name of a categorical attribute, followed by, for each category used in that attribute, the category number, the desired name for the category, and a CSS color (like "white" or "#FFFFFF" or "rgb(255,255,255)) to use to represent that category. Fields on each line are separated by tab characters. + + </help> +</tool> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/inflate.svg Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="29.851999" + height="32.582985" + id="svg2" + version="1.1" + inkscape:version="0.48.3.1 r9886" + sodipodi:docname="inflate.svg"> + <defs + id="defs21"> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send" + style="overflow:visible"> + <path + id="path3801" + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-4" + style="overflow:visible"> + <path + inkscape:connector-curvature="0" + id="path3801-6" + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-2" + style="overflow:visible"> + <path + inkscape:connector-curvature="0" + id="path3801-9" + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0" + refX="0" + id="Arrow1Send-8" + style="overflow:visible"> + <path + inkscape:connector-curvature="0" + id="path3801-3" + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt" + transform="matrix(-0.2,0,0,-0.2,-1.2,0)" /> + </marker> + </defs> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1301" + inkscape:window-height="704" + id="namedview19" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="2.5041624" + inkscape:cx="120.51589" + inkscape:cy="48.987443" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> + <g + id="g4" + transform="matrix(0.03970783,0,0,0.03959906,7.8677399,8.123235)"> + <title + id="title6">Layer 1</title> + <g + transform="translate(-1.26562,-1.26172)" + id="imagebot_119"> + <path + id="imagebot_120" + d="m 187.07201,461.681 c 0,0 -9.594,-11.483 -9.594,-33.191 l -8.296,0 c 0,21.708 -9.596,33.191 -9.596,33.191" + inkscape:connector-curvature="0" + style="fill:#bed63a;stroke:#515c19;stroke-width:2.52600002" /> + <path + id="imagebot_121" + d="m 345.39499,185.959 c 0,129.11999 -97.513,242.795 -172.065,242.795 -71.68901,0 -172.066,-103.295 -172.066,-242.795 C 1.263,83.955 71.325,1.263 173.33,1.263 c 102.00301,0 172.06499,82.692 172.06499,184.696 z" + inkscape:connector-curvature="0" + style="fill:#bed63a;stroke:#515c19;stroke-width:2.52600002" /> + <ellipse + id="imagebot_122" + ry="3.8180001" + rx="13.743" + cy="461.681" + cx="173.32899" + d="m 187.07199,461.681 c 0,2.10862 -6.15295,3.818 -13.743,3.818 -7.59004,0 -13.743,-1.70938 -13.743,-3.818 0,-2.10862 6.15296,-3.818 13.743,-3.818 7.59005,0 13.743,1.70938 13.743,3.818 z" + sodipodi:cx="173.32899" + sodipodi:cy="461.681" + sodipodi:rx="13.743" + sodipodi:ry="3.8180001" + style="fill:#bed63a;stroke:#515c19;stroke-width:2.52600002" /> + </g> + </g> + <g + id="g12" + transform="matrix(0.03970783,0,0,0.03959906,7.8677399,8.123235)"> + <title + id="title14">Layer 2</title> + <path + id="imagebot_123" + d="m 179.307,28.2213 c 0,0 125.037,65.676 95.988,286.7007 0,0 63.527,-103.315 32.838,-188.187 C 274.6,33.9963 179.307,28.2213 179.307,28.2213 z" + inkscape:connector-curvature="0" + style="fill:#d6e584" /> + </g> + <metadata + id="metadata17">image/svg+xmlOpen Clip Art LibraryBalloon2010-07-21T15:06:35Simple balloon illustration.http://openclipart.org/detail/74221/balloon-by-jgm104jgm104balloonchildcircusclip artclipartfloatingrubbertoy<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> + <path + style="fill:none;stroke:#000000;stroke-width:1.72656119;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Send)" + d="M 14.855292,9.3428471 14.51008,2.4575339" + id="path5690" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#000000;stroke-width:1.72656119;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Send)" + d="m 20.47967,17.357859 6.912193,-0.09543" + id="path5690-7" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-20.021171" + inkscape:transform-center-y="-1.9396242" /> + <path + style="fill:none;stroke:#000000;stroke-width:1.72656119;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Send)" + d="m 14.67395,23.229042 0.05797,6.893672" + id="path5690-7-8" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-1.8418242" + inkscape:transform-center-y="19.978895" /> + <path + style="fill:none;stroke:#000000;stroke-width:1.72656119;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Send)" + d="M 9.3659788,17.191706 2.458586,17.465695" + id="path5690-7-88" + inkscape:connector-curvature="0" + inkscape:transform-center-x="19.96343" + inkscape:transform-center-y="2.4268093" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/jquery.tsv.js Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/jstat-1.0.0.js Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/layer_5.tab~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,20 @@ +GSE14206_GPL8_stage=T3A_PHENOTYPE.tab 0 +GSE18655_GPL5858_recurrence=No_Rec_PHENOTYPE.tab 0 +GSE21034_GPL10264_pathological_stage=T4_PHENOTYPE.tab 1 +TCGA_tumor_level=Middle_PHENOTYPE.tab 1 +GSE14206_GPL8_stage=T2A_PHENOTYPE.tab 1 +TCGA_clinical_spread_ct2=Induration_and+or_Nodularity_Involves___or_=__?_of_one_lobe__cT2a__PHENOTYPE.tab 0 +GSE21034_GPL10264_ERG-fusion_gex=True_PHENOTYPE.tab 1 +GSE14206_GPL8_stage=T2C_PHENOTYPE.tab 0 +GSE18655_GPL5858_age_MEAN.tab 1 +GSE18655_GPL5858_psa_MEAN.tab 1 +GSE21034_GPL10264_Gene_fusion=True_PHENOTYPE.tab 0 +GSE14206_GPL8_stage=T3B_PHENOTYPE.tab 0 +GSE21034_GPL10264_pathological_stage=NA_PHENOTYPE.tab 0 +TCGA_zone_of_origin=Peripheral_Zone_PHENOTYPE.tab 1 +TCGA_diagnostic_mri_results=Extraprostatic_Extension_Localized__e.g._seminal_vesicles__PHENOTYPE.tab 0 +TCGA_pathologic_spread_pt4=YES_PHENOTYPE.tab 1 +TCGA_shortest_dimension_MEAN.tab 1 +GSE21034_GPL10264_clint_stage=T2C_PHENOTYPE.tab 1 +TCGA_diagnostic_ct_abd_pelvis_performed=YES_PHENOTYPE.tab 1 +GSE14206_GPL887_ets_group=ESE3_Low_PHENOTYPE.tab 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/layers.tab~ Tue Oct 22 14:17:59 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 +zebulon 1 layer_4.tab 2 20 12 +xylophone 2 layer_5.tab 2 20 11
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/maplabel-compiled.js Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/matrix_0.tab~ Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/right.svg Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/save.svg Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,192 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Sodipodi ("http://www.sodipodi.com/") --> + +<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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + height="27.1875" + id="svg548" + sodipodi:docname="save.svg" + sodipodi:version="0.32" + width="27" + version="1.1" + inkscape:version="0.48.3.1 r9886"> + <metadata + id="metadata3"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:title /> + <dc:description /> + <dc:subject> + <rdf:Bag> + <rdf:li>hash</rdf:li> + <rdf:li /> + <rdf:li>computer</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:publisher> + <cc:Agent + rdf:about="http://www.openclipart.org"> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:publisher> + <dc:creator> + <cc:Agent> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:rights> + <dc:date /> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <cc:license + rdf:resource="http://web.resource.org/cc/PublicDomain" /> + <dc:language>en</dc:language> + </cc:Work> + <cc:License + rdf:about="http://web.resource.org/cc/PublicDomain"> + <cc:permits + rdf:resource="http://web.resource.org/cc/Reproduction" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/Distribution" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/DerivativeWorks" /> + </cc:License> + </rdf:RDF> + </metadata> + <defs + id="defs550" /> + <sodipodi:namedview + id="base" + showgrid="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="2.6700352" + inkscape:cx="69.677527" + inkscape:cy="52.709901" + inkscape:window-width="1301" + inkscape:window-height="704" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="1" + inkscape:current-layer="svg548" /> + <path + d="m 0.13698097,0.1343605 24.24897403,0 2.494464,2.4364954 L 26.764453,27.051871 0.19499143,26.935837 0.13698347,0.1343605 z" + id="path551" + style="fill:#00007b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.25656179;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + <path + d="m 21.949439,0.2592358 0,7.2212882 c 0,0 -0.174051,1.069755 -1.102258,1.404103 C 19.919044,8.951417 7.2724631,8.951497 7.2724631,8.951497 7.2144471,9.018367 6.4602916,8.41654 6.4022745,8.015401 6.3442585,7.614184 6.2862402,7.480524 6.2862402,7.480524 c 0,0 0.1160343,-7.2881498 0.058016,-7.2881498 -0.058016,0 15.6631988,0.066862 15.6051818,0.066862 z" + id="path552" + sodipodi:nodetypes="cccccccc" + style="fill:#a3a6a6;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.17104124;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + <rect + height="12.704596" + id="rect553" + style="font-size:12px;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="21.98649" + x="2.689502" + y="13.535099" /> + <rect + height="7.7837553" + id="rect554" + style="font-size:12px;fill:#808080;fill-rule:evenodd;stroke-width:1pt" + width="0" + x="300.18646" + y="157.24586" /> + <path + d="m 19.454906,1.6426701 c -0.05802,0 -3.596723,0 -3.596723,0 l 0,6.0912619 3.596723,0 0,-6.0912619 z" + id="path555" + style="fill:#00007b;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.17104119;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + <rect + height="3.7707691" + id="rect556" + style="font-size:12px;fill:#dd8080;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="21.986492" + x="2.689502" + y="13.535099" /> + <rect + height="0.46409538" + id="rect557" + style="font-size:12px;fill:#dd8080;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="19.666018" + x="3.7337143" + y="19.394289" /> + <rect + height="0.4060829" + id="rect558" + style="font-size:12px;fill:#dd8080;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="19.666018" + x="3.6757019" + y="21.830795" /> + <rect + height="0.4060829" + id="rect559" + style="font-size:12px;fill:#df8080;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="19.608006" + x="3.7337143" + y="24.267286" /> + <rect + height="0.34806943" + id="rect567" + style="font-size:12px;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="5.3951015" + x="7.4464741" + y="1.5846567" /> + <rect + height="0.34807205" + id="rect568" + style="font-size:12px;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="4.0028186" + x="7.5624957" + y="2.4548321" /> + <rect + height="0.34806943" + id="rect569" + style="font-size:12px;fill:#180003;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="5.221067" + x="7.5624957" + y="3.3250132" /> + <rect + height="0.40608501" + id="rect570" + style="font-size:12px;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="7.8315992" + x="3.7917266" + y="14.34726" /> + <rect + height="0.34807047" + id="rect571" + style="font-size:12px;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="3.0166156" + x="3.8497388" + y="15.44949" /> + <rect + height="0.34806964" + id="rect572" + style="font-size:12px;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:1pt" + width="2.3204749" + x="8.7227325" + y="15.44949" /> + <path + d="M 6.824404,7.265759 C 6.9326388,6.940986 6.7161008,0.7705103 7.1491084,0.7705103 c 0.4330076,0 9.4181436,-0.1082481 9.4181436,-0.1082481 C 12.453574,1.0952764 7.7986548,0.9870216 6.824404,7.265759 z" + id="path566" + sodipodi:nodetypes="cccc" + style="fill:#ffffff;fill-opacity:0.5;fill-rule:evenodd;stroke:none" + inkscape:connector-curvature="0" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/select2.css Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/select2.js Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/set.svg Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,201 @@ +<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="24.647392" + height="23.09375" + id="svg2" + version="1.1" + inkscape:version="0.48.3.1 r9886" + sodipodi:docname="set.svg"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="11.2" + inkscape:cx="37.729826" + inkscape:cy="19.396497" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1301" + inkscape:window-height="704" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + showborder="true" /> + <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(3.8022401,-1030.1184)"> + <path + transform="matrix(1.2211561,0,0,1.2211561,-71.653905,1020.2289)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4140-5" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + transform="matrix(1.2211561,0,0,1.2211561,-61.796763,1023.3539)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4140-8" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + transform="matrix(1.2211561,0,0,1.2211561,-68.011048,1028.5324)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4140-58" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4207" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-71.653905,1020.2289)" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4209" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-61.796763,1023.3539)" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4211" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-68.011048,1028.5324)" /> + <path + transform="matrix(1.2211561,0,0,1.2211561,-71.653905,1020.2289)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4213" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + transform="matrix(1.2211561,0,0,1.2211561,-61.796763,1023.3539)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4215" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + transform="matrix(1.2211561,0,0,1.2211561,-68.011048,1028.5324)" + sodipodi:type="arc" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + id="path4217" + sodipodi:cx="61.619305" + sodipodi:cy="14.154314" + sodipodi:rx="5.5558391" + sodipodi:ry="5.5558391" + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4219" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-71.653905,1020.2289)" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4221" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-61.796763,1023.3539)" /> + <path + d="m 67.175144,14.154314 c 0,3.068405 -2.487434,5.555839 -5.555839,5.555839 -3.068406,0 -5.555839,-2.487434 -5.555839,-5.555839 0,-3.068405 2.487433,-5.555839 5.555839,-5.555839 3.068405,0 5.555839,2.487434 5.555839,5.555839 z" + sodipodi:ry="5.5558391" + sodipodi:rx="5.5558391" + sodipodi:cy="14.154314" + sodipodi:cx="61.619305" + id="path4223" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + sodipodi:type="arc" + transform="matrix(1.2211561,0,0,1.2211561,-68.011048,1028.5324)" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.22115612;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + d="m 7.2357427,1039.0448 c -3.0094933,0 -5.5495545,1.9435 -6.43749994,4.6562 0.85243504,0.386 1.78462804,0.5938 2.78124994,0.5938 3.0101183,0 5.5813115,-1.9427 6.4687503,-4.6563 -0.8541528,-0.3879 -1.8132998,-0.5937 -2.8125003,-0.5937 z" + id="path4225" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.22115612;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + d="m 9.8294927,1034.8885 c -1.9093309,1.2005 -3.15625,3.3281 -3.15625,5.75 0,0.9263 0.1951408,1.8206 0.53125,2.625 1.9093309,-1.2005 3.1875003,-3.3281 3.1875003,-5.75 0,-0.9267 -0.226159,-1.8203 -0.5625003,-2.625 z" + id="path4227" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.22115612;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + d="m 7.2357427,1039.0448 c -0.1279582,0 -0.2487893,0.024 -0.375,0.031 -0.1175521,0.5003 -0.1875,1.0264 -0.1875,1.5625 0,3.747 3.034248,6.7813 6.7812493,6.7813 0.127958,0 0.248789,-0.024 0.375,-0.031 0.121981,-0.509 0.1875,-1.0161 0.1875,-1.5625 0,-3.747 -3.034247,-6.7812 -6.7812493,-6.7812 z" + id="path4233" + inkscape:connector-curvature="0" /> + <path + style="fill:#008000;fill-opacity:1;stroke:#008000;stroke-width:1.22115612;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + d="m 7.2357427,1039.0448 c -0.1289522,0 -0.2478646,0.024 -0.375,0.031 -0.058776,0.2501 -0.092819,0.5202 -0.125,0.7813 -0.028269,0.2535 -0.0625,0.52 -0.0625,0.7812 0,0.4632 0.034201,0.9038 0.125,1.3438 0.00197,0.01 -0.00201,0.021 0,0.031 0.044283,0.2099 0.1248265,0.4229 0.1875,0.625 0.00287,0.01 -0.00291,0.022 0,0.031 0.060514,0.1922 0.1429028,0.3788 0.21875,0.5625 0.089436,-0.056 0.1946942,-0.096 0.28125,-0.1563 0.042386,-0.03 0.083322,-0.063 0.125,-0.094 0.096544,-0.071 0.1886771,-0.1435 0.28125,-0.2188 0.1773902,-0.1449 0.3380704,-0.3069 0.5,-0.4687 0.2204067,-0.2199 0.4366288,-0.4391 0.625,-0.6875 0.022654,-0.03 0.040328,-0.064 0.0625,-0.094 0.1981108,-0.2705 0.3747891,-0.5759 0.53125,-0.875 0.00632,-0.012 0.025005,-0.019 0.03125,-0.031 0.150422,-0.2921 0.268182,-0.5911 0.3750003,-0.9063 0.0045,-0.013 0.02682,-0.018 0.03125,-0.031 -0.063662,-0.029 -0.1227913,-0.066 -0.1875003,-0.094 -0.161782,-0.068 -0.3008294,-0.1316 -0.46875,-0.1875 -0.04079,-0.013 -0.083853,-0.019 -0.125,-0.031 -0.070794,-0.022 -0.1469806,-0.043 -0.21875,-0.063 -0.1464042,-0.039 -0.2873032,-0.095 -0.4375,-0.125 -0.4415675,-0.09 -0.9066248,-0.125 -1.375,-0.125 z" + id="path4241" + inkscape:connector-curvature="0" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/statistics.js Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,709 @@ +// 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 if (is_discrete) { + // This is a multinomial/categorical layer + // TODO: statistics for discrete non-binary layers + return NaN; + } else { + // This is a continuous layer, so run a t test + return t_compare(layer_data, in_list, out_list, all_list); + } + +} + +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 t_compare(layer_data, in_list, out_list, all_list) { + // Given the data of a continuous layer object (an object from signature + // name to float (or undefined)), and arrays of the names of "in" and "out" + // signatures, do a t test test for whether the in signatures differ from + // the out signatures. Returns an object of metadata, with "p_value" set to + // either the p value of the test (two-tailed), or NaN if the test cannot be + // performed (due to, e.g. fewer than 2 samples in one category). + + // Go through the in list and calculate all the summary statistics + // How many non-NaN values? + var number_in = 0; + // What is the sum? + var sum_in = 0; + + for(var i = 0; i < in_list.length; i++) { + if(!isNaN(layer_data[in_list[i]])) { + number_in++; + sum_in += layer_data[in_list[i]]; + } + } + + // We've done one pass, so we know if we have any in list data actually + if(number_in < 2) { + // Not enough to run the t test + return NaN; + } + + // What is the mean? + var mean_in = sum_in / number_in; + + // What is the second moment (sum of squares of differences from the mean) + var second_moment_in = 0; + for(var i = 0; i < in_list.length; i++) { + if(!isNaN(layer_data[in_list[i]])) { + second_moment_in += Math.pow(layer_data[in_list[i]] - mean_in, 2); + } + } + + // What is the unbiased variance? + unbiased_variance_in = second_moment_in / (number_in - 1); + + // Now go through the same process for the out list + // How many non-NaN values? + var number_out = 0; + // What is the sum? + var sum_out = 0; + + for(var i = 0; i < out_list.length; i++) { + if(!isNaN(layer_data[out_list[i]])) { + number_out++; + sum_out += layer_data[out_list[i]]; + } + } + + // We've done one pass, so we know if we have any out list data actually + if(number_out < 2) { + // Not enough to run the t test + return NaN; + } + + // What is the mean? + var mean_out = sum_out / number_out; + + // What is the second moment (sum of squares of differences from the mean) + var second_moment_out = 0; + for(var i = 0; i < out_list.length; i++) { + if(!isNaN(layer_data[out_list[i]])) { + second_moment_out += Math.pow(layer_data[out_list[i]] - mean_out, + 2); + } + } + + // What is the unbiased variance? + unbiased_variance_out = second_moment_out / (number_out - 1); + + // We can't do the test if both variances are 0 + if(unbiased_variance_in == 0 && unbiased_variance_out == 0) { + return NaN; + } + + // Now we can calculate the t test two-tailed p value + var p_value = t_test(mean_in, unbiased_variance_in, number_in, mean_out, + unbiased_variance_out, number_out); + + // And return it in a dict with other metadata. + // We don't really have any other metadata. + return { + p_value: p_value + }; +} + +function t_test(mean_in, unbiased_variance_in, number_in, mean_out, + unbiased_variance_out, number_out) { + + // Given the mean, unbiased variance, and number of samples for both the in + // group and the out group, compute the p value for the t test with unequal + // sample sizes and unequal variances, testing to see whether the means + // differ (a two-tailed "Welch's" t test). See + // https://en.wikipedia.org/wiki/Student%27s_t-test + // Assumes we have enough samples to actually perform the test. + + // First, calculate the t statistic, which is where our observations fall on + // the t distribution. + var t_statistic = (mean_in - mean_out) / Math.sqrt((unbiased_variance_in / + number_in) + (unbiased_variance_out / number_out)); + + + // Calculate the degrees of freedom for the particular t distribution that + // we ought to compare the statistic against + var degrees_of_freedom = Math.pow((unbiased_variance_in / number_in) + + (unbiased_variance_out / number_out), 2) / + ((Math.pow(unbiased_variance_in / number_in, 2) / (number_in - 1)) + + (Math.pow(unbiased_variance_out / number_out, 2) / (number_out - 1))); + + // Now we have to compare the t statistic to the t test CDF available via + // the totally undocumented jstat.pt = function(q, df, ncp, lower_tail, log) + // where: + // q is the t statistic value to calculate the cdf at + // df is the degrees of freedom + // ncp is the "mu" parameter for the t distributiuon. I think this sets the + // mean, and it's OK to leave blank. + // lower_tail presumably specifies if we want the lower or upper tail of the + // CDF. Defaults to true. + // Log specifies if we want the log probability. Defaults to false. + + // Make the t statistic be on the low side of the distribution, and + // calculate the lower tail's area using the CDF. + var one_tail_probability = jstat.pt(0 - Math.abs(t_statistic), + degrees_of_freedom); + + // Return the two-tailed p value, which, since the t distribution is + // symmetric, is just twice the single-tail probability + return 2 * one_tail_probability; + +} + +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 Returns an object of metadata, + // with "p_value" set to either the p value of the test (two-tailed), or NaN + // if the test cannot be performed. 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 our p value as "p_value", and also how many non-pseudocount + // successes were in the in_list and the out_list. + return { + p_value: p, + inside_yes: inside_yes, + outside_yes: outside_yes + }; +} + +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-6ae12361157c/hexagram/statistics.js~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,709 @@ +// 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 if (is_discrete) { + // This is a multinomial/categorical layer + // TODO: statistics for discrete non-binary layers + return NaN; + } else { + // This is a continuous layer, so run a t test + return t_compare(layer_data, in_list, out_list, all_list); + } + +} + +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 t_compare(layer_data, in_list, out_list, all_list) { + // Given the data of a continuous layer object (an object from signature + // name to float (or undefined)), and arrays of the names of "in" and "out" + // signatures, do a t test test for whether the in signatures differ from + // the out signatures. Returns an object of metadata, with "p_value" set to + // either the p value of the test (two-tailed), or NaN if the test cannot be + // performed (due to, e.g. fewer than 2 samples in one category). + + // Go through the in list and calculate all the summary statistics + // How many non-NaN values? + var number_in = 0; + // What is the sum? + var sum_in = 0; + + for(var i = 0; i < in_list.length; i++) { + if(!isNaN(layer_data[in_list[i]])) { + number_in++; + sum_in += layer_data[in_list[i]]; + } + } + + // We've done one pass, so we know if we have any in list data actually + if(number_in < 2) { + // Not enough to run the t test + return NaN; + } + + // What is the mean? + var mean_in = sum_in / number_in; + + // What is the second moment (sum of squares of differences from the mean) + var second_moment_in = 0; + for(var i = 0; i < in_list.length; i++) { + if(!isNaN(layer_data[in_list[i]])) { + second_moment_in += Math.pow(layer_data[in_list[i]] - mean_in, 2); + } + } + + // What is the unbiased variance? + unbiased_variance_in = second_moment_in / (number_in - 1); + + // Now go through the same process for the out list + // How many non-NaN values? + var number_out = 0; + // What is the sum? + var sum_out = 0; + + for(var i = 0; i < out_list.length; i++) { + if(!isNaN(layer_data[out_list[i]])) { + number_out++; + sum_out += layer_data[out_list[i]]; + } + } + + // We've done one pass, so we know if we have any out list data actually + if(number_out < 2) { + // Not enough to run the t test + return NaN; + } + + // What is the mean? + var mean_out = sum_out / number_out; + + // What is the second moment (sum of squares of differences from the mean) + var second_moment_out = 0; + for(var i = 0; i < out_list.length; i++) { + if(!isNaN(layer_data[out_list[i]])) { + second_moment_out += Math.pow(layer_data[out_list[i]] - mean_out, + 2); + } + } + + // What is the unbiased variance? + unbiased_variance_out = second_moment_out / (number_out - 1); + + // We can't do the test if both variances are 0 + if(unbiased_variance_in == 0 && unbiased_variance_out == 0) { + return NaN; + } + + // Now we can calculate the t test two-tailed p value + var p_value = t_test(mean_in, unbiased_variance_in, number_in, mean_out, + unbiased_variance_out, number_out); + + // And return it in a dict with other metadata. + // We don't really have any other metadata. + return { + p_value: p_value + }; +} + +function t_test(mean_in, unbiased_variance_in, number_in, mean_out, + unbiased_variance_out, number_out) { + + // Given the mean, unbiased variance, and number of samples for both the in + // group and the out group, compute the p value for the t test with unequal + // sample sizes and unequal variances, testing to see whether the means + // differ (a two-tailed "Welch's" t test). See + // https://en.wikipedia.org/wiki/Student%27s_t-test + // Assumes we have enough samples to actually perform the test. + + // First, calculate the t statistic, which is where our observations fall on + // the t distribution. + var t_statistic = (mean_in - mean_out) / Math.sqrt((unbiased_variance_in / + number_in) + (unbiased_variance_out / number_out)); + + + // Calculate the degrees of freedom for the particular t distribution that + // we ought to compare the statistic against + var degrees_of_freedom = Math.pow((unbiased_variance_in / number_in) + + (unbiased_variance_out / number_out), 2) / + ((Math.pow(unbiased_variance_in / number_in, 2) / (number_in - 1)) + + (Math.pow(unbiased_variance_out / number_out, 2) / (number_out - 1))); + + // Now we have to compare the t statistic to the t test CDF available via + // the totally undocumented jstat.pt = function(q, df, ncp, lower_tail, log) + // where: + // q is the t statistic value to calculate the cdf at + // df is the degrees of freedom + // ncp is the "mu" parameter for the t distributiuon. I think this sets the + // mean, and it's OK to leave blank. + // lower_tail presumably specifies if we want the lower or upper tail of the + // CDF. Defaults to true. + // Log specifies if we want the log probability. Defaults to false. + + // Make the t statistic be on the low side of the distribution, and + // calculate the lower tail's area using the CDF. + var one_tail_probability = jstat.pt(0 - Math.abs(t_statistic), + degrees_of_freedom); + + // Return the two-tailed p value, which, since the t distribution is + // symmetric, is just twice the single-tail probability + return 2 * one_tail_probability; + +} + +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 Returns an object of metadata, + // with "p_value" set to either the p value of the test (two-tailed), or NaN + // if the test cannot be performed. 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 our p value as "p_value", and also how many non-pseudocount + // successes were in the in_list and the out_list. + return { + p_value: p, + "1s in A": inside_yes, + "1s in background": outside_yes + }; +} + +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-6ae12361157c/hexagram/statistics.svg Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/throbber.svg Tue Oct 22 14:17:59 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-6ae12361157c/hexagram/tool_dependencies.xml Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,40 @@ +<?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> + <!-- + Now we can access DrL tools like truncate (at the expense of + GNU truncate) + --> + <!-- + TODO: report to Galaxy that comments as the last element of + a set_enviromnent action atag are not properly handeled. + See install_util.py line 435 in Galaxy revision 9d42f1e32efb + --> + <action type="set_environment"> + <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable> + </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-6ae12361157c/hexagram/tool_dependencies.xml~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,40 @@ +<?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> + <!-- + Now we can access DrL tools like truncate (at the expense of + GNU truncate) + --> + <!-- + TODO: report to Galaxy that comments as the last element of + a set_enviromnent action atag are not properly handeled. + See install_util.py line 435 in revision 9d42f1e32efb + --> + <action type="set_environment"> + <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable> + </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-6ae12361157c/hexagram/tools.js Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,389 @@ +// 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; + } + }); + }); +}); + +$(function() { + // Set up the link to this page control + add_tool("link-to-page", "Link to this Page...", function() { + + // We will provide the user with an alert box with the link to the + // hexagrap visualization map. + + var link = (window.location.protocol + "//" + window.location.host + + "/" + window.location.pathname); + + alert(link); + selected_tool = undefined; + + }); +}); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/tools.js~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,373 @@ +// 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 + print("Not putting any text"); + 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-6ae12361157c/hexagram/tsv.py Tue Oct 22 14:17:59 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()