diff --git a/assets/css/locuszoom.scss b/assets/css/locuszoom.scss index 31195fff..7deb7fed 100644 --- a/assets/css/locuszoom.scss +++ b/assets/css/locuszoom.scss @@ -77,41 +77,41 @@ svg.#{$namespace}-locuszoom { stroke-width: 1px; } - path.#{$namespace}-data_layer-scatter { + path.#{$namespace}-data_layer-scatter, path.#{$namespace}-data_layer-category_scatter { stroke: #{$default_black_shadow}; stroke-opacity: #{$default_black_shadow_opacity}; stroke-width: 1px; cursor: pointer; } - path.#{$namespace}-data_layer-scatter-highlighted { + path.#{$namespace}-data_layer-scatter-highlighted, path.#{$namespace}-data_layer-category_scatter-highlighted { stroke: #{$default_black_shadow}; stroke-opacity: #{$default_black_shadow_opacity}; stroke-width: 4px; } - path.#{$namespace}-data_layer-scatter-selected { + path.#{$namespace}-data_layer-scatter-selected, path.#{$namespace}-data_layer-category_scatter-selected { stroke: #{$default_black}; stroke-opacity: #{$default_black_opacity}; stroke-width: 4px; } - path.#{$namespace}-data_layer-scatter-faded { + path.#{$namespace}-data_layer-scatter-faded, path.#{$namespace}-data_layer-category_scatter-faded { fill-opacity: 0.1; stroke-opacity: 0.1; } - path.#{$namespace}-data_layer-scatter-hidden { + path.#{$namespace}-data_layer-scatter-hidden, path.#{$namespace}-data_layer-category_scatter-hidden { display: none; } - text.#{$namespace}-data_layer-scatter-label { + text.#{$namespace}-data_layer-scatter-label, text.#{$namespace}-data_layer-category_scatter-label { fill: #{$default_black}; fill-opacity: #{$default_black_opacity}; alignment-baseline: middle; } - line.#{$namespace}-data_layer-scatter-label { + line.#{$namespace}-data_layer-scatter-label, line.#{$namespace}-data_layer-category_scatter-label { stroke: #{$default_black}; stroke-opacity: #{$default_black_opacity}; stroke-width: 1px; diff --git a/assets/js/app/Data.js b/assets/js/app/Data.js index 23f9106f..703065df 100644 --- a/assets/js/app/Data.js +++ b/assets/js/app/Data.js @@ -237,8 +237,17 @@ LocusZoom.Data.Requester = function(sources) { * @public */ LocusZoom.Data.Source = function() { - /** @member {Boolean} */ + /** + * Whether this source should enable caching + * @member {Boolean} + */ this.enableCache = true; + /** + * Whether this data source type is dependent on previous requests- for example, the LD source cannot annotate + * association data if no data was found for that region. + * @member {boolean} + */ + this.dependentSource = false; }; /** @@ -333,11 +342,18 @@ LocusZoom.Data.Source.prototype.getData = function(state, fields, outnames, tran } } + var self = this; return function (chain) { - return this.getRequest(state, chain, fields).then(function(resp) { - return this.parseResponse(resp, chain, fields, outnames, trans); - }.bind(this)); - }.bind(this); + if (self.dependentSource && chain && chain.body && !chain.body.length) { + // A "dependent" source should not attempt to fire a request if there is no data for it to act on. + // Therefore, it should simply return the previous data chain. + return Q.when(chain); + } + + return self.getRequest(state, chain, fields).then(function(resp) { + return self.parseResponse(resp, chain, fields, outnames, trans); + }); + }; }; /** @@ -554,12 +570,15 @@ LocusZoom.Data.AssociationSource.prototype.getURL = function(state, chain, field /** * Data Source for LD Data, as fetched from the LocusZoom API server (or compatible) + * This source is designed to connect its results to association data, and therefore depends on association data having + * been loaded by a previous request in the data chain. * @class * @public * @augments LocusZoom.Data.Source */ LocusZoom.Data.LDSource = LocusZoom.Data.Source.extend(function(init) { this.parseInit(init); + this.dependentSource = true; }, "LDLZ"); LocusZoom.Data.LDSource.prototype.preGetData = function(state, fields) { diff --git a/assets/js/app/DataLayer.js b/assets/js/app/DataLayer.js index 1284fe10..01e57853 100644 --- a/assets/js/app/DataLayer.js +++ b/assets/js/app/DataLayer.js @@ -35,6 +35,12 @@ LocusZoom.DataLayer = function(layout, parent) { if (this.layout.x_axis !== {} && typeof this.layout.x_axis.axis !== "number"){ this.layout.x_axis.axis = 1; } if (this.layout.y_axis !== {} && typeof this.layout.y_axis.axis !== "number"){ this.layout.y_axis.axis = 1; } + /** + * Values in the layout object may change during rendering etc. Retain a copy of the original data layer state + * @member {Object} + */ + this._base_layout = JSON.parse(JSON.stringify(this.layout)); + /** @member {Object} */ this.state = {}; /** @member {String} */ @@ -354,52 +360,54 @@ LocusZoom.DataLayer.prototype.getAxisExtent = function(dimension){ throw("Invalid dimension identifier passed to LocusZoom.DataLayer.getAxisExtent()"); } - var axis = dimension + "_axis"; + var axis_name = dimension + "_axis"; + var axis_layout = this.layout[axis_name]; // If a floor AND a ceiling are explicitly defined then just return that extent and be done - if (!isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].ceiling)){ - return [+this.layout[axis].floor, +this.layout[axis].ceiling]; + if (!isNaN(axis_layout.floor) && !isNaN(axis_layout.ceiling)){ + return [+axis_layout.floor, +axis_layout.ceiling]; } // If a field is defined for the axis and the data layer has data then generate the extent from the data set - if (this.layout[axis].field && this.data && this.data.length){ - - var extent = d3.extent(this.data, function(d) { - var f = new LocusZoom.Data.Field(this.layout[axis].field); - return +f.resolve(d); - }.bind(this)); - - // Apply floor/ceiling - if (!isNaN(this.layout[axis].floor)) { - extent[0] = this.layout[axis].floor; - extent[1] = d3.max(extent); - } - if (!isNaN(this.layout[axis].ceiling)) { - extent[1] = this.layout[axis].ceiling; - extent[0] = d3.min(extent); - } - - // Apply upper/lower buffers, if applicable - var original_extent_span = extent[1] - extent[0]; - if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].lower_buffer)) { - extent[0] -= original_extent_span * this.layout[axis].lower_buffer; - } - if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].upper_buffer)) { - extent[1] += original_extent_span * this.layout[axis].upper_buffer; - } - - // Apply minimum extent - if (typeof this.layout[axis].min_extent == "object") { - if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].min_extent[0])) { - extent[0] = Math.min(extent[0], this.layout[axis].min_extent[0]); + var data_extent = []; + if (axis_layout.field && this.data) { + if (!this.data.length) { + // If data has been fetched (but no points in region), enforce the min_extent (with no buffers, + // because we don't need padding around an empty screen) + data_extent = axis_layout.min_extent || []; + return data_extent; + } else { + data_extent = d3.extent(this.data, function (d) { + var f = new LocusZoom.Data.Field(axis_layout.field); + return +f.resolve(d); + }); + + // Apply upper/lower buffers, if applicable + var original_extent_span = data_extent[1] - data_extent[0]; + if (!isNaN(axis_layout.lower_buffer)) { + data_extent[0] -= original_extent_span * axis_layout.lower_buffer; } - if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].min_extent[1])) { - extent[1] = Math.max(extent[1], this.layout[axis].min_extent[1]); + if (!isNaN(axis_layout.upper_buffer)) { + data_extent[1] += original_extent_span * axis_layout.upper_buffer; } - } - - return extent; + if (typeof axis_layout.min_extent == "object") { + // The data should span at least the range specified by min_extent, an array with [low, high] + var range_min = axis_layout.min_extent[0]; + var range_max = axis_layout.min_extent[1]; + if (!isNaN(range_min) && !isNaN(range_max)) { + data_extent[0] = Math.min(data_extent[0], range_min); + } + if (!isNaN(range_max)) { + data_extent[1] = Math.max(data_extent[1], range_max); + } + } + // If specified, floor and ceiling will override the actual data range + return [ + isNaN(axis_layout.floor) ? data_extent[0] : axis_layout.floor, + isNaN(axis_layout.ceiling) ? data_extent[1] : axis_layout.ceiling + ]; + } } // If this is for the x axis and no extent could be generated yet but state has a defined start and end diff --git a/assets/js/app/DataLayers/scatter.js b/assets/js/app/DataLayers/scatter.js index 94e035d1..cd230e00 100644 --- a/assets/js/app/DataLayers/scatter.js +++ b/assets/js/app/DataLayers/scatter.js @@ -435,6 +435,11 @@ LocusZoom.DataLayers.add("scatter", function(layout){ this.flip_labels(); this.seperate_iterations = 0; this.separate_labels(); + // Apply default event emitters to selection + this.label_texts.on("click.event_emitter", function(element){ + this.parent.emit("element_clicked", element); + this.parent_plot.emit("element_clicked", element); + }.bind(this)); // Extend mouse behaviors to labels this.applyBehaviors(this.label_texts); } @@ -504,8 +509,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { }, /** - * Identify the unique categories on the plot, and update the layout with an appropriate color scheme - * + * Identify the unique categories on the plot, and update the layout with an appropriate color scheme. * Also identify the min and max x value associated with the category, which will be used to generate ticks * @private * @returns {Object.} Series of entries used to build category name ticks {category_name: [min_x, max_x]} @@ -524,16 +528,67 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { }); var categoryNames = Object.keys(uniqueCategories); - // Construct a color scale with a sufficient number of visually distinct colors - // TODO: This will break for more than 20 categories in a single API response payload for a single PheWAS plot - var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20; - var colors = color_scale().range().slice(0, categoryNames.length); // List of hex values, should be of same length as categories array + this._setDynamicColorScheme(categoryNames); - this.layout.color.parameters.categories = categoryNames; - this.layout.color.parameters.values = colors; return uniqueCategories; }, + /** + * Automatically define a color scheme for the layer based on data returned from the server. + * If part of the color scheme has been specified, it will fill in remaining missing information. + * + * There are three scenarios: + * 1. The layout does not specify either category names or (color) values. Dynamically build both based on + * the data and update the layout. + * 2. The layout specifies colors, but not categories. Use that exact color information provided, and dynamically + * determine what categories are present in the data. (cycle through the available colors, reusing if there + * are a lot of categories) + * 3. The layout specifies exactly what colors and categories to use (and they match the data!). This is useful to + * specify an explicit mapping between color scheme and category names, when you want to be sure that the + * plot matches a standard color scheme. + * (If the layout specifies categories that do not match the data, the user specified categories will be ignored) + * + * This method will only act if the layout defines a `categorical_bin` scale function for coloring. It may be + * overridden in a subclass to suit other types of coloring methods. + * + * @param {String[]} categoryNames + * @private + */ + _setDynamicColorScheme: function(categoryNames) { + var colorParams = this.layout.color.parameters; + var baseParams = this._base_layout.color.parameters; + + // If the layout does not use a supported coloring scheme, or is already complete, this method should do nothing + if (this.layout.color.scale_function !== "categorical_bin") { + throw "This layer requires that coloring be specified as a `categorical_bin`"; + } + + if (baseParams.categories.length && baseParams.values.length) { + // If there are preset category/color combos, make sure that they apply to the actual dataset + var parameters_categories_hash = {}; + baseParams.categories.forEach(function (category) { parameters_categories_hash[category] = 1; }); + if (categoryNames.every(function (name) { return parameters_categories_hash.hasOwnProperty(name); })) { + // The layout doesn't have to specify categories in order, but make sure they are all there + colorParams.categories = baseParams.categories; + } else { + colorParams.categories = categoryNames; + } + } else { + colorParams.categories = categoryNames; + } + // Prefer user-specified colors if provided. Make sure that there are enough colors for all the categories. + var colors; + if (baseParams.values.length) { + colors = baseParams.values; + } else { + var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20; + colors = color_scale().range(); + } + while (colors.length < categoryNames.length) { colors = colors.concat(colors); } + colors = colors.slice(0, categoryNames.length); // List of hex values, should be of same length as categories array + colorParams.values = colors; + }, + /** * * @param dimension @@ -561,6 +616,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { if (dimension === "x") { // If colors have been defined by this layer, use them to make tick colors match scatterplot point colors + var knownCategories = this.layout.color.parameters.categories || []; var knownColors = this.layout.color.parameters.values || []; return Object.keys(categoryBounds).map(function (category, index) { @@ -584,7 +640,7 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { x: xPos, text: category, style: { - "fill": knownColors[index] || "#000000" + "fill": knownColors[knownCategories.indexOf(category)] || "#000000" } }; }); @@ -600,4 +656,4 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { this._categories = this._generateCategoryBounds(); return this; } -}); \ No newline at end of file +}); diff --git a/assets/js/app/Layouts.js b/assets/js/app/Layouts.js index 7a363875..b4ba25ca 100644 --- a/assets/js/app/Layouts.js +++ b/assets/js/app/Layouts.js @@ -361,7 +361,9 @@ LocusZoom.Layouts.add("data_layer", "phewas_pvalues", { fields: ["{{namespace[phewas]}}id", "{{namespace[phewas]}}log_pvalue", "{{namespace[phewas]}}trait_group", "{{namespace[phewas]}}trait_label"], x_axis: { field: "{{namespace[phewas]}}x", // Synthetic/derived field added by `category_scatter` layer - category_field: "{{namespace[phewas]}}trait_group" + category_field: "{{namespace[phewas]}}trait_group", + lower_buffer: 0.025, + upper_buffer: 0.025 }, y_axis: { axis: 1, diff --git a/assets/js/app/LocusZoom.js b/assets/js/app/LocusZoom.js index d5f687d8..e5eccb67 100644 --- a/assets/js/app/LocusZoom.js +++ b/assets/js/app/LocusZoom.js @@ -2,7 +2,7 @@ * @namespace */ var LocusZoom = { - version: "0.7.1" + version: "0.7.2" }; /** diff --git a/assets/js/app/wrapper.txt b/assets/js/app/wrapper.txt index 1f5dbe88..d475e7ad 100644 --- a/assets/js/app/wrapper.txt +++ b/assets/js/app/wrapper.txt @@ -41,7 +41,8 @@ throw("Q dependency not met. Library missing."); } - <%= contents %> + // ESTemplate: module content goes here + ;%= body %; } catch (plugin_loading_error){ console.error("LocusZoom Plugin error: " + plugin_loading_error); diff --git a/dist/locuszoom.app.js b/dist/locuszoom.app.js index 2452f431..6f84ebe3 100644 --- a/dist/locuszoom.app.js +++ b/dist/locuszoom.app.js @@ -1,54 +1,47 @@ (function (root, factory) { - if (typeof define === "function" && define.amd) { - define(["postal"], function(d3, Q){ - return (root.LocusZoom = factory(d3, Q)); + if (typeof define === 'function' && define.amd) { + define(['postal'], function (d3, Q) { + return root.LocusZoom = factory(d3, Q); }); - } else if(typeof module === "object" && module.exports) { - module.exports = (root.LocusZoom = factory(require("d3"), require("Q"))); + } else if (typeof module === 'object' && module.exports) { + module.exports = root.LocusZoom = factory(require('d3'), require('Q')); } else { root.LocusZoom = factory(root.d3, root.Q); } -}(this, function(d3, Q) { - - var semanticVersionIsOk = function(minimum_version, current_version){ +}(this, function (d3, Q) { + var semanticVersionIsOk = function (minimum_version, current_version) { // handle the trivial case - if (current_version == minimum_version){ return true; } + if (current_version == minimum_version) { + return true; + } // compare semantic versions by component as integers // compare semantic versions by component as integers - var minimum_version_array = minimum_version.split("."); - var current_version_array = current_version.split("."); + var minimum_version_array = minimum_version.split('.'); + var current_version_array = current_version.split('.'); var version_is_ok = false; - minimum_version_array.forEach(function(d, i){ - if (!version_is_ok && +current_version_array[i] > +minimum_version_array[i]){ + minimum_version_array.forEach(function (d, i) { + if (!version_is_ok && +current_version_array[i] > +minimum_version_array[i]) { version_is_ok = true; } }); return version_is_ok; }; - try { - // Verify dependency: d3.js - var minimum_d3_version = "3.5.6"; - if (typeof d3 != "object"){ - throw("d3 dependency not met. Library missing."); - } - if (!semanticVersionIsOk(minimum_d3_version, d3.version)){ - throw("d3 dependency not met. Outdated version detected.\nRequired d3 version: " + minimum_d3_version + " or higher (found: " + d3.version + ")."); + var minimum_d3_version = '3.5.6'; + if (typeof d3 != 'object') { + throw 'd3 dependency not met. Library missing.'; } - + if (!semanticVersionIsOk(minimum_d3_version, d3.version)) { + throw 'd3 dependency not met. Outdated version detected.\nRequired d3 version: ' + minimum_d3_version + ' or higher (found: ' + d3.version + ').'; + } // Verify dependency: Q.js // Verify dependency: Q.js - if (typeof Q != "function"){ - throw("Q dependency not met. Library missing."); - } - + if (typeof Q != 'function') { + throw 'Q dependency not met. Library missing.'; + } // ESTemplate: module content goes here + // ESTemplate: module content goes here + ; + var LocusZoom = { version: '0.7.2' }; /** - * @namespace - */ -var LocusZoom = { - version: "0.7.1" -}; - -/** * Populate a single element with a LocusZoom plot. * selector can be a string for a DOM Query or a d3 selector. * @param {String} selector CSS selector for the container element where the plot will be mounted. Any pre-existing @@ -57,50 +50,46 @@ var LocusZoom = { * @param {Object} layout A JSON-serializable object of layout configuration parameters * @returns {LocusZoom.Plot} The newly created plot instance */ -LocusZoom.populate = function(selector, datasource, layout) { - if (typeof selector == "undefined"){ - throw ("LocusZoom.populate selector not defined"); - } - // Empty the selector of any existing content - d3.select(selector).html(""); - var plot; - d3.select(selector).call(function(){ - // Require each containing element have an ID. If one isn't present, create one. - if (typeof this.node().id == "undefined"){ - var iterator = 0; - while (!d3.select("#lz-" + iterator).empty()){ iterator++; } - this.attr("id", "#lz-" + iterator); - } - // Create the plot - plot = new LocusZoom.Plot(this.node().id, datasource, layout); - plot.container = this.node(); - // Detect data-region and fill in state values if present - if (typeof this.node().dataset !== "undefined" && typeof this.node().dataset.region !== "undefined"){ - var parsed_state = LocusZoom.parsePositionQuery(this.node().dataset.region); - Object.keys(parsed_state).forEach(function(key){ - plot.state[key] = parsed_state[key]; + LocusZoom.populate = function (selector, datasource, layout) { + if (typeof selector == 'undefined') { + throw 'LocusZoom.populate selector not defined'; + } + // Empty the selector of any existing content + d3.select(selector).html(''); + var plot; + d3.select(selector).call(function () { + // Require each containing element have an ID. If one isn't present, create one. + if (typeof this.node().id == 'undefined') { + var iterator = 0; + while (!d3.select('#lz-' + iterator).empty()) { + iterator++; + } + this.attr('id', '#lz-' + iterator); + } + // Create the plot + plot = new LocusZoom.Plot(this.node().id, datasource, layout); + plot.container = this.node(); + // Detect data-region and fill in state values if present + if (typeof this.node().dataset !== 'undefined' && typeof this.node().dataset.region !== 'undefined') { + var parsed_state = LocusZoom.parsePositionQuery(this.node().dataset.region); + Object.keys(parsed_state).forEach(function (key) { + plot.state[key] = parsed_state[key]; + }); + } + // Add an SVG to the div and set its dimensions + plot.svg = d3.select('div#' + plot.id).append('svg').attr('version', '1.1').attr('xmlns', 'http://www.w3.org/2000/svg').attr('id', plot.id + '_svg').attr('class', 'lz-locuszoom').style(plot.layout.style); + plot.setDimensions(); + plot.positionPanels(); + // Initialize the plot + plot.initialize(); + // If the plot has defined data sources then trigger its first mapping based on state values + if (typeof datasource == 'object' && Object.keys(datasource).length) { + plot.refresh(); + } }); - } - // Add an SVG to the div and set its dimensions - plot.svg = d3.select("div#" + plot.id) - .append("svg") - .attr("version", "1.1") - .attr("xmlns", "http://www.w3.org/2000/svg") - .attr("id", plot.id + "_svg").attr("class", "lz-locuszoom") - .style(plot.layout.style); - plot.setDimensions(); - plot.positionPanels(); - // Initialize the plot - plot.initialize(); - // If the plot has defined data sources then trigger its first mapping based on state values - if (typeof datasource == "object" && Object.keys(datasource).length){ - plot.refresh(); - } - }); - return plot; -}; - -/** + return plot; + }; + /** * Populate arbitrarily many elements each with a LocusZoom plot * using a common datasource and layout * @param {String} selector CSS selector for the container element where the plot will be mounted. Any pre-existing @@ -109,15 +98,14 @@ LocusZoom.populate = function(selector, datasource, layout) { * @param {Object} layout A JSON-serializable object of layout configuration parameters * @returns {LocusZoom.Plot[]} */ -LocusZoom.populateAll = function(selector, datasource, layout) { - var plots = []; - d3.selectAll(selector).each(function(d,i) { - plots[i] = LocusZoom.populate(this, datasource, layout); - }); - return plots; -}; - -/** + LocusZoom.populateAll = function (selector, datasource, layout) { + var plots = []; + d3.selectAll(selector).each(function (d, i) { + plots[i] = LocusZoom.populate(this, datasource, layout); + }); + return plots; + }; + /** * Convert an integer chromosome position to an SI string representation (e.g. 23423456 => "23.42" (Mb)) * @param {Number} pos Position * @param {String} [exp] Exponent to use for the returned string, eg 6=> MB. If not specified, will attempt to guess @@ -125,87 +113,89 @@ LocusZoom.populateAll = function(selector, datasource, layout) { * @param {Boolean} [suffix=false] Whether or not to append a suffix (e.g. "Mb") to the end of the returned string * @returns {string} */ -LocusZoom.positionIntToString = function(pos, exp, suffix){ - var exp_symbols = { 0: "", 3: "K", 6: "M", 9: "G" }; - suffix = suffix || false; - if (isNaN(exp) || exp === null){ - var log = Math.log(pos) / Math.LN10; - exp = Math.min(Math.max(log - (log % 3), 0), 9); - } - var places_exp = exp - Math.floor((Math.log(pos) / Math.LN10).toFixed(exp + 3)); - var min_exp = Math.min(Math.max(exp, 0), 2); - var places = Math.min(Math.max(places_exp, min_exp), 12); - var ret = "" + (pos / Math.pow(10, exp)).toFixed(places); - if (suffix && typeof exp_symbols[exp] !== "undefined"){ - ret += " " + exp_symbols[exp] + "b"; - } - return ret; -}; - -/** + LocusZoom.positionIntToString = function (pos, exp, suffix) { + var exp_symbols = { + 0: '', + 3: 'K', + 6: 'M', + 9: 'G' + }; + suffix = suffix || false; + if (isNaN(exp) || exp === null) { + var log = Math.log(pos) / Math.LN10; + exp = Math.min(Math.max(log - log % 3, 0), 9); + } + var places_exp = exp - Math.floor((Math.log(pos) / Math.LN10).toFixed(exp + 3)); + var min_exp = Math.min(Math.max(exp, 0), 2); + var places = Math.min(Math.max(places_exp, min_exp), 12); + var ret = '' + (pos / Math.pow(10, exp)).toFixed(places); + if (suffix && typeof exp_symbols[exp] !== 'undefined') { + ret += ' ' + exp_symbols[exp] + 'b'; + } + return ret; + }; + /** * Convert an SI string chromosome position to an integer representation (e.g. "5.8 Mb" => 58000000) * @param {String} p The chromosome position * @returns {Number} */ -LocusZoom.positionStringToInt = function(p) { - var val = p.toUpperCase(); - val = val.replace(/,/g, ""); - var suffixre = /([KMG])[B]*$/; - var suffix = suffixre.exec(val); - var mult = 1; - if (suffix) { - if (suffix[1]==="M") { - mult = 1e6; - } else if (suffix[1]==="G") { - mult = 1e9; - } else { - mult = 1e3; //K - } - val = val.replace(suffixre,""); - } - val = Number(val) * mult; - return val; -}; - -/** + LocusZoom.positionStringToInt = function (p) { + var val = p.toUpperCase(); + val = val.replace(/,/g, ''); + var suffixre = /([KMG])[B]*$/; + var suffix = suffixre.exec(val); + var mult = 1; + if (suffix) { + if (suffix[1] === 'M') { + mult = 1000000; + } else if (suffix[1] === 'G') { + mult = 1000000000; + } else { + mult = 1000; //K + } + val = val.replace(suffixre, ''); + } + val = Number(val) * mult; + return val; + }; + /** * Parse region queries into their constituent parts * TODO: handle genes (or send off to API) * @param {String} x A chromosome position query. May be any of the forms `chr:start-end`, `chr:center+offset`, * or `chr:pos` * @returns {{chr:*, start: *, end:*} | {chr:*, position:*}} */ -LocusZoom.parsePositionQuery = function(x) { - var chrposoff = /^(\w+):([\d,.]+[kmgbKMGB]*)([-+])([\d,.]+[kmgbKMGB]*)$/; - var chrpos = /^(\w+):([\d,.]+[kmgbKMGB]*)$/; - var match = chrposoff.exec(x); - if (match) { - if (match[3] === "+") { - var center = LocusZoom.positionStringToInt(match[2]); - var offset = LocusZoom.positionStringToInt(match[4]); - return { - chr:match[1], - start: center - offset, - end: center + offset - }; - } else { - return { - chr: match[1], - start: LocusZoom.positionStringToInt(match[2]), - end: LocusZoom.positionStringToInt(match[4]) - }; - } - } - match = chrpos.exec(x); - if (match) { - return { - chr:match[1], - position: LocusZoom.positionStringToInt(match[2]) + LocusZoom.parsePositionQuery = function (x) { + var chrposoff = /^(\w+):([\d,.]+[kmgbKMGB]*)([-+])([\d,.]+[kmgbKMGB]*)$/; + var chrpos = /^(\w+):([\d,.]+[kmgbKMGB]*)$/; + var match = chrposoff.exec(x); + if (match) { + if (match[3] === '+') { + var center = LocusZoom.positionStringToInt(match[2]); + var offset = LocusZoom.positionStringToInt(match[4]); + return { + chr: match[1], + start: center - offset, + end: center + offset + }; + } else { + return { + chr: match[1], + start: LocusZoom.positionStringToInt(match[2]), + end: LocusZoom.positionStringToInt(match[4]) + }; + } + } + match = chrpos.exec(x); + if (match) { + return { + chr: match[1], + position: LocusZoom.positionStringToInt(match[2]) + }; + } + return null; }; - } - return null; -}; - -/** + /** * Generate a "pretty" set of ticks (multiples of 1, 2, or 5 on the same order of magnitude for the range) * Based on R's "pretty" function: https://github.com/wch/r-source/blob/b156e3a711967f58131e23c1b1dc1ea90e2f0c43/src/appl/pretty.c * @param {Number[]} range A two-item array specifying [low, high] values for the axis range @@ -217,65 +207,66 @@ LocusZoom.parsePositionQuery = function(x) { * @param {Number} [target_tick_count=5] The approximate number of ticks you would like to be returned; may not be exact * @returns {Number[]} */ -LocusZoom.prettyTicks = function(range, clip_range, target_tick_count){ - if (typeof target_tick_count == "undefined" || isNaN(parseInt(target_tick_count))){ - target_tick_count = 5; - } - target_tick_count = parseInt(target_tick_count); - - var min_n = target_tick_count / 3; - var shrink_sml = 0.75; - var high_u_bias = 1.5; - var u5_bias = 0.5 + 1.5 * high_u_bias; - - var d = Math.abs(range[0] - range[1]); - var c = d / target_tick_count; - if ((Math.log(d) / Math.LN10) < -2){ - c = (Math.max(Math.abs(d)) * shrink_sml) / min_n; - } - - var base = Math.pow(10, Math.floor(Math.log(c)/Math.LN10)); - var base_toFixed = 0; - if (base < 1 && base !== 0){ - base_toFixed = Math.abs(Math.round(Math.log(base)/Math.LN10)); - } - - var unit = base; - if ( ((2 * base) - c) < (high_u_bias * (c - unit)) ){ - unit = 2 * base; - if ( ((5 * base) - c) < (u5_bias * (c - unit)) ){ - unit = 5 * base; - if ( ((10 * base) - c) < (high_u_bias * (c - unit)) ){ - unit = 10 * base; + LocusZoom.prettyTicks = function (range, clip_range, target_tick_count) { + if (typeof target_tick_count == 'undefined' || isNaN(parseInt(target_tick_count))) { + target_tick_count = 5; } - } - } - - var ticks = []; - var i = parseFloat( (Math.floor(range[0]/unit)*unit).toFixed(base_toFixed) ); - while (i < range[1]){ - ticks.push(i); - i += unit; - if (base_toFixed > 0){ - i = parseFloat(i.toFixed(base_toFixed)); - } - } - ticks.push(i); - - if (typeof clip_range == "undefined" || ["low", "high", "both", "neither"].indexOf(clip_range) === -1){ - clip_range = "neither"; - } - if (clip_range === "low" || clip_range === "both"){ - if (ticks[0] < range[0]){ ticks = ticks.slice(1); } - } - if (clip_range === "high" || clip_range === "both"){ - if (ticks[ticks.length-1] > range[1]){ ticks.pop(); } - } - - return ticks; -}; - -/** + target_tick_count = parseInt(target_tick_count); + var min_n = target_tick_count / 3; + var shrink_sml = 0.75; + var high_u_bias = 1.5; + var u5_bias = 0.5 + 1.5 * high_u_bias; + var d = Math.abs(range[0] - range[1]); + var c = d / target_tick_count; + if (Math.log(d) / Math.LN10 < -2) { + c = Math.max(Math.abs(d)) * shrink_sml / min_n; + } + var base = Math.pow(10, Math.floor(Math.log(c) / Math.LN10)); + var base_toFixed = 0; + if (base < 1 && base !== 0) { + base_toFixed = Math.abs(Math.round(Math.log(base) / Math.LN10)); + } + var unit = base; + if (2 * base - c < high_u_bias * (c - unit)) { + unit = 2 * base; + if (5 * base - c < u5_bias * (c - unit)) { + unit = 5 * base; + if (10 * base - c < high_u_bias * (c - unit)) { + unit = 10 * base; + } + } + } + var ticks = []; + var i = parseFloat((Math.floor(range[0] / unit) * unit).toFixed(base_toFixed)); + while (i < range[1]) { + ticks.push(i); + i += unit; + if (base_toFixed > 0) { + i = parseFloat(i.toFixed(base_toFixed)); + } + } + ticks.push(i); + if (typeof clip_range == 'undefined' || [ + 'low', + 'high', + 'both', + 'neither' + ].indexOf(clip_range) === -1) { + clip_range = 'neither'; + } + if (clip_range === 'low' || clip_range === 'both') { + if (ticks[0] < range[0]) { + ticks = ticks.slice(1); + } + } + if (clip_range === 'high' || clip_range === 'both') { + if (ticks[ticks.length - 1] > range[1]) { + ticks.pop(); + } + } + return ticks; + }; + /** * Make an AJAX request and return a promise. * From http://www.html5rocks.com/en/tutorials/cors/ * and with promises from https://gist.github.com/kriskowal/593076 @@ -287,46 +278,45 @@ LocusZoom.prettyTicks = function(range, clip_range, target_tick_count){ * @param {Number} [timeout] If provided, wait this long (in ms) before timing out * @returns {Promise} */ -LocusZoom.createCORSPromise = function (method, url, body, headers, timeout) { - var response = Q.defer(); - var xhr = new XMLHttpRequest(); - if ("withCredentials" in xhr) { - // Check if the XMLHttpRequest object has a "withCredentials" property. - // "withCredentials" only exists on XMLHTTPRequest2 objects. - xhr.open(method, url, true); - } else if (typeof XDomainRequest != "undefined") { - // Otherwise, check if XDomainRequest. - // XDomainRequest only exists in IE, and is IE's way of making CORS requests. - xhr = new XDomainRequest(); - xhr.open(method, url); - } else { - // Otherwise, CORS is not supported by the browser. - xhr = null; - } - if (xhr) { - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status === 200 || xhr.status === 0 ) { - response.resolve(xhr.response); - } else { - response.reject("HTTP " + xhr.status + " for " + url); + LocusZoom.createCORSPromise = function (method, url, body, headers, timeout) { + var response = Q.defer(); + var xhr = new XMLHttpRequest(); + if ('withCredentials' in xhr) { + // Check if the XMLHttpRequest object has a "withCredentials" property. + // "withCredentials" only exists on XMLHTTPRequest2 objects. + xhr.open(method, url, true); + } else if (typeof XDomainRequest != 'undefined') { + // Otherwise, check if XDomainRequest. + // XDomainRequest only exists in IE, and is IE's way of making CORS requests. + xhr = new XDomainRequest(); + xhr.open(method, url); + } else { + // Otherwise, CORS is not supported by the browser. + xhr = null; + } + if (xhr) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200 || xhr.status === 0) { + response.resolve(xhr.response); + } else { + response.reject('HTTP ' + xhr.status + ' for ' + url); + } + } + }; + timeout && setTimeout(response.reject, timeout); + body = typeof body !== 'undefined' ? body : ''; + if (typeof headers !== 'undefined') { + for (var header in headers) { + xhr.setRequestHeader(header, headers[header]); + } } + // Send the request + xhr.send(body); } + return response.promise; }; - timeout && setTimeout(response.reject, timeout); - body = typeof body !== "undefined" ? body : ""; - if (typeof headers !== "undefined"){ - for (var header in headers){ - xhr.setRequestHeader(header, headers[header]); - } - } - // Send the request - xhr.send(body); - } - return response.promise; -}; - -/** + /** * Validate a (presumed complete) plot state object against internal rules for consistency, and ensure the plot fits * within any constraints imposed by the layout. * @param {Object} new_state @@ -335,64 +325,59 @@ LocusZoom.createCORSPromise = function (method, url, body, headers, timeout) { * @param {Object} layout * @returns {*|{}} */ -LocusZoom.validateState = function(new_state, layout){ - - new_state = new_state || {}; - layout = layout || {}; - - // If a "chr", "start", and "end" are present then resolve start and end - // to numeric values that are not decimal, negative, or flipped - var validated_region = false; - if (typeof new_state.chr != "undefined" && typeof new_state.start != "undefined" && typeof new_state.end != "undefined"){ - // Determine a numeric scale and midpoint for the attempted region, - var attempted_midpoint = null; var attempted_scale; - new_state.start = Math.max(parseInt(new_state.start), 1); - new_state.end = Math.max(parseInt(new_state.end), 1); - if (isNaN(new_state.start) && isNaN(new_state.end)){ - new_state.start = 1; - new_state.end = 1; - attempted_midpoint = 0.5; - attempted_scale = 0; - } else if (isNaN(new_state.start) || isNaN(new_state.end)){ - attempted_midpoint = new_state.start || new_state.end; - attempted_scale = 0; - new_state.start = (isNaN(new_state.start) ? new_state.end : new_state.start); - new_state.end = (isNaN(new_state.end) ? new_state.start : new_state.end); - } else { - attempted_midpoint = Math.round((new_state.start + new_state.end) / 2); - attempted_scale = new_state.end - new_state.start; - if (attempted_scale < 0){ - var temp = new_state.start; - new_state.end = new_state.start; - new_state.start = temp; - attempted_scale = new_state.end - new_state.start; - } - if (attempted_midpoint < 0){ - new_state.start = 1; - new_state.end = 1; - attempted_scale = 0; + LocusZoom.validateState = function (new_state, layout) { + new_state = new_state || {}; + layout = layout || {}; + // If a "chr", "start", and "end" are present then resolve start and end + // to numeric values that are not decimal, negative, or flipped + var validated_region = false; + if (typeof new_state.chr != 'undefined' && typeof new_state.start != 'undefined' && typeof new_state.end != 'undefined') { + // Determine a numeric scale and midpoint for the attempted region, + var attempted_midpoint = null; + var attempted_scale; + new_state.start = Math.max(parseInt(new_state.start), 1); + new_state.end = Math.max(parseInt(new_state.end), 1); + if (isNaN(new_state.start) && isNaN(new_state.end)) { + new_state.start = 1; + new_state.end = 1; + attempted_midpoint = 0.5; + attempted_scale = 0; + } else if (isNaN(new_state.start) || isNaN(new_state.end)) { + attempted_midpoint = new_state.start || new_state.end; + attempted_scale = 0; + new_state.start = isNaN(new_state.start) ? new_state.end : new_state.start; + new_state.end = isNaN(new_state.end) ? new_state.start : new_state.end; + } else { + attempted_midpoint = Math.round((new_state.start + new_state.end) / 2); + attempted_scale = new_state.end - new_state.start; + if (attempted_scale < 0) { + var temp = new_state.start; + new_state.end = new_state.start; + new_state.start = temp; + attempted_scale = new_state.end - new_state.start; + } + if (attempted_midpoint < 0) { + new_state.start = 1; + new_state.end = 1; + attempted_scale = 0; + } + } + validated_region = true; } - } - validated_region = true; - } - - // Constrain w/r/t layout-defined minimum region scale - if (!isNaN(layout.min_region_scale) && validated_region && attempted_scale < layout.min_region_scale){ - new_state.start = Math.max(attempted_midpoint - Math.floor(layout.min_region_scale / 2), 1); - new_state.end = new_state.start + layout.min_region_scale; - } - - // Constrain w/r/t layout-defined maximum region scale - if (!isNaN(layout.max_region_scale) && validated_region && attempted_scale > layout.max_region_scale){ - new_state.start = Math.max(attempted_midpoint - Math.floor(layout.max_region_scale / 2), 1); - new_state.end = new_state.start + layout.max_region_scale; - } - - return new_state; -}; - -// -/** + // Constrain w/r/t layout-defined minimum region scale + if (!isNaN(layout.min_region_scale) && validated_region && attempted_scale < layout.min_region_scale) { + new_state.start = Math.max(attempted_midpoint - Math.floor(layout.min_region_scale / 2), 1); + new_state.end = new_state.start + layout.min_region_scale; + } + // Constrain w/r/t layout-defined maximum region scale + if (!isNaN(layout.max_region_scale) && validated_region && attempted_scale > layout.max_region_scale) { + new_state.start = Math.max(attempted_midpoint - Math.floor(layout.max_region_scale / 2), 1); + new_state.end = new_state.start + layout.max_region_scale; + } + return new_state; + }; + // + /** * Replace placeholders in an html string with field values defined in a data object * Only works on scalar values! Will ignore non-scalars. * @@ -404,134 +389,164 @@ LocusZoom.validateState = function(new_state, layout){ * Since this is only an existence check, **variables with a value of 0 will be evaluated as true**. * @returns {string} */ -LocusZoom.parseFields = function (data, html) { - if (typeof data != "object"){ - throw ("LocusZoom.parseFields invalid arguments: data is not an object"); - } - if (typeof html != "string"){ - throw ("LocusZoom.parseFields invalid arguments: html is not a string"); - } - // `tokens` is like [token,...] - // `token` is like {text: '...'} or {variable: 'foo|bar'} or {condition: 'foo|bar'} or {close: 'if'} - var tokens = []; - var regex = /\{\{(?:(#if )?([A-Za-z0-9_:|]+)|(\/if))\}\}/; - while (html.length > 0){ - var m = regex.exec(html); - if (!m) { tokens.push({text: html}); html = ""; } - else if (m.index !== 0) { tokens.push({text: html.slice(0, m.index)}); html = html.slice(m.index); } - else if (m[1] === "#if ") { tokens.push({condition: m[2]}); html = html.slice(m[0].length); } - else if (m[2]) { tokens.push({variable: m[2]}); html = html.slice(m[0].length); } - else if (m[3] === "/if") { tokens.push({close: "if"}); html = html.slice(m[0].length); } - else { - console.error("Error tokenizing tooltip when remaining template is " + JSON.stringify(html) + - " and previous tokens are " + JSON.stringify(tokens) + - " and current regex match is " + JSON.stringify([m[1], m[2], m[3]])); - html=html.slice(m[0].length); - } - } - var astify = function() { - var token = tokens.shift(); - if (typeof token.text !== "undefined" || token.variable) { - return token; - } else if (token.condition) { - token.then = []; - while(tokens.length > 0) { - if (tokens[0].close === "if") { tokens.shift(); break; } - token.then.push(astify()); - } - return token; - } else { - console.error("Error making tooltip AST due to unknown token " + JSON.stringify(token)); - return { text: "" }; - } - }; - // `ast` is like [thing,...] - // `thing` is like {text: "..."} or {variable:"foo|bar"} or {condition: "foo|bar", then:[thing,...]} - var ast = []; - while (tokens.length > 0) ast.push(astify()); - - var resolve = function(variable) { - if (!resolve.cache.hasOwnProperty(variable)) { - resolve.cache[variable] = (new LocusZoom.Data.Field(variable)).resolve(data); - } - return resolve.cache[variable]; - }; - resolve.cache = {}; - var render_node = function(node) { - if (typeof node.text !== "undefined") { - return node.text; - } else if (node.variable) { - try { - var value = resolve(node.variable); - if (["string","number","boolean"].indexOf(typeof value) !== -1) { return value; } - if (value === null) { return ""; } - } catch (error) { console.error("Error while processing variable " + JSON.stringify(node.variable)); } - return "{{" + node.variable + "}}"; - } else if (node.condition) { - try { - var condition = resolve(node.condition); - if (condition || condition === 0) { - return node.then.map(render_node).join(""); + LocusZoom.parseFields = function (data, html) { + if (typeof data != 'object') { + throw 'LocusZoom.parseFields invalid arguments: data is not an object'; + } + if (typeof html != 'string') { + throw 'LocusZoom.parseFields invalid arguments: html is not a string'; + } + // `tokens` is like [token,...] + // `token` is like {text: '...'} or {variable: 'foo|bar'} or {condition: 'foo|bar'} or {close: 'if'} + var tokens = []; + var regex = /\{\{(?:(#if )?([A-Za-z0-9_:|]+)|(\/if))\}\}/; + while (html.length > 0) { + var m = regex.exec(html); + if (!m) { + tokens.push({ text: html }); + html = ''; + } else if (m.index !== 0) { + tokens.push({ text: html.slice(0, m.index) }); + html = html.slice(m.index); + } else if (m[1] === '#if ') { + tokens.push({ condition: m[2] }); + html = html.slice(m[0].length); + } else if (m[2]) { + tokens.push({ variable: m[2] }); + html = html.slice(m[0].length); + } else if (m[3] === '/if') { + tokens.push({ close: 'if' }); + html = html.slice(m[0].length); + } else { + console.error('Error tokenizing tooltip when remaining template is ' + JSON.stringify(html) + ' and previous tokens are ' + JSON.stringify(tokens) + ' and current regex match is ' + JSON.stringify([ + m[1], + m[2], + m[3] + ])); + html = html.slice(m[0].length); } - } catch (error) { console.error("Error while processing condition " + JSON.stringify(node.variable)); } - return ""; - } else { console.error("Error rendering tooltip due to unknown AST node " + JSON.stringify(node)); } - }; - return ast.map(render_node).join(""); -}; - -/** + } + var astify = function () { + var token = tokens.shift(); + if (typeof token.text !== 'undefined' || token.variable) { + return token; + } else if (token.condition) { + token.then = []; + while (tokens.length > 0) { + if (tokens[0].close === 'if') { + tokens.shift(); + break; + } + token.then.push(astify()); + } + return token; + } else { + console.error('Error making tooltip AST due to unknown token ' + JSON.stringify(token)); + return { text: '' }; + } + }; + // `ast` is like [thing,...] + // `thing` is like {text: "..."} or {variable:"foo|bar"} or {condition: "foo|bar", then:[thing,...]} + var ast = []; + while (tokens.length > 0) + ast.push(astify()); + var resolve = function (variable) { + if (!resolve.cache.hasOwnProperty(variable)) { + resolve.cache[variable] = new LocusZoom.Data.Field(variable).resolve(data); + } + return resolve.cache[variable]; + }; + resolve.cache = {}; + var render_node = function (node) { + if (typeof node.text !== 'undefined') { + return node.text; + } else if (node.variable) { + try { + var value = resolve(node.variable); + if ([ + 'string', + 'number', + 'boolean' + ].indexOf(typeof value) !== -1) { + return value; + } + if (value === null) { + return ''; + } + } catch (error) { + console.error('Error while processing variable ' + JSON.stringify(node.variable)); + } + return '{{' + node.variable + '}}'; + } else if (node.condition) { + try { + var condition = resolve(node.condition); + if (condition || condition === 0) { + return node.then.map(render_node).join(''); + } + } catch (error) { + console.error('Error while processing condition ' + JSON.stringify(node.variable)); + } + return ''; + } else { + console.error('Error rendering tooltip due to unknown AST node ' + JSON.stringify(node)); + } + }; + return ast.map(render_node).join(''); + }; + /** * Shortcut method for getting the data bound to a tool tip. * @param {Element} node * @returns {*} The first element of data bound to the tooltip */ -LocusZoom.getToolTipData = function(node){ - if (typeof node != "object" || typeof node.parentNode == "undefined"){ - throw("Invalid node object"); - } - // If this node is a locuszoom tool tip then return its data - var selector = d3.select(node); - if (selector.classed("lz-data_layer-tooltip") && typeof selector.data()[0] != "undefined"){ - return selector.data()[0]; - } else { - return LocusZoom.getToolTipData(node.parentNode); - } -}; - -/** + LocusZoom.getToolTipData = function (node) { + if (typeof node != 'object' || typeof node.parentNode == 'undefined') { + throw 'Invalid node object'; + } + // If this node is a locuszoom tool tip then return its data + var selector = d3.select(node); + if (selector.classed('lz-data_layer-tooltip') && typeof selector.data()[0] != 'undefined') { + return selector.data()[0]; + } else { + return LocusZoom.getToolTipData(node.parentNode); + } + }; + /** * Shortcut method for getting a reference to the data layer that generated a tool tip. * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip * @returns {LocusZoom.DataLayer} */ -LocusZoom.getToolTipDataLayer = function(node){ - var data = LocusZoom.getToolTipData(node); - if (data.getDataLayer){ return data.getDataLayer(); } - return null; -}; - -/** + LocusZoom.getToolTipDataLayer = function (node) { + var data = LocusZoom.getToolTipData(node); + if (data.getDataLayer) { + return data.getDataLayer(); + } + return null; + }; + /** * Shortcut method for getting a reference to the panel that generated a tool tip. * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip * @returns {LocusZoom.Panel} */ -LocusZoom.getToolTipPanel = function(node){ - var data_layer = LocusZoom.getToolTipDataLayer(node); - if (data_layer){ return data_layer.parent; } - return null; -}; - -/** + LocusZoom.getToolTipPanel = function (node) { + var data_layer = LocusZoom.getToolTipDataLayer(node); + if (data_layer) { + return data_layer.parent; + } + return null; + }; + /** * Shortcut method for getting a reference to the plot that generated a tool tip. * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip * @returns {LocusZoom.Plot} */ -LocusZoom.getToolTipPlot = function(node){ - var panel = LocusZoom.getToolTipPanel(node); - if (panel){ return panel.parent; } - return null; -}; - -/** + LocusZoom.getToolTipPlot = function (node) { + var panel = LocusZoom.getToolTipPanel(node); + if (panel) { + return panel.parent; + } + return null; + }; + /** * Generate a curtain object for a plot, panel, or any other subdivision of a layout * The panel curtain, like the plot curtain is an HTML overlay that obscures the entire panel. It can be styled * arbitrarily and display arbitrary messages. It is useful for reporting error messages visually to an end user @@ -539,89 +554,87 @@ LocusZoom.getToolTipPlot = function(node){ * TODO: Improve type doc here * @returns {object} */ -LocusZoom.generateCurtain = function(){ - var curtain = { - showing: false, - selector: null, - content_selector: null, - hide_delay: null, - - /** + LocusZoom.generateCurtain = function () { + var curtain = { + showing: false, + selector: null, + content_selector: null, + hide_delay: null, + /** * Generate the curtain. Any content (string) argument passed will be displayed in the curtain as raw HTML. * CSS (object) can be passed which will apply styles to the curtain and its content. * @param {string} content Content to be displayed on the curtain (as raw HTML) * @param {object} css Apply the specified styles to the curtain and its contents */ - show: function(content, css){ - if (!this.curtain.showing){ - this.curtain.selector = d3.select(this.parent_plot.svg.node().parentNode).insert("div") - .attr("class", "lz-curtain").attr("id", this.id + ".curtain"); - this.curtain.content_selector = this.curtain.selector.append("div").attr("class", "lz-curtain-content"); - this.curtain.selector.append("div").attr("class", "lz-curtain-dismiss").html("Dismiss") - .on("click", function(){ - this.curtain.hide(); - }.bind(this)); - this.curtain.showing = true; - } - return this.curtain.update(content, css); - }.bind(this), - - /** + show: function (content, css) { + if (!this.curtain.showing) { + this.curtain.selector = d3.select(this.parent_plot.svg.node().parentNode).insert('div').attr('class', 'lz-curtain').attr('id', this.id + '.curtain'); + this.curtain.content_selector = this.curtain.selector.append('div').attr('class', 'lz-curtain-content'); + this.curtain.selector.append('div').attr('class', 'lz-curtain-dismiss').html('Dismiss').on('click', function () { + this.curtain.hide(); + }.bind(this)); + this.curtain.showing = true; + } + return this.curtain.update(content, css); + }.bind(this), + /** * Update the content and css of the curtain that's currently being shown. This method also adjusts the size * and positioning of the curtain to ensure it still covers the entire panel with no overlap. * @param {string} content Content to be displayed on the curtain (as raw HTML) * @param {object} css Apply the specified styles to the curtain and its contents */ - update: function(content, css){ - if (!this.curtain.showing){ return this.curtain; } - clearTimeout(this.curtain.hide_delay); - // Apply CSS if provided - if (typeof css == "object"){ - this.curtain.selector.style(css); - } - // Update size and position - var page_origin = this.getPageOrigin(); - this.curtain.selector.style({ - top: page_origin.y + "px", - left: page_origin.x + "px", - width: this.layout.width + "px", - height: this.layout.height + "px" - }); - this.curtain.content_selector.style({ - "max-width": (this.layout.width - 40) + "px", - "max-height": (this.layout.height - 40) + "px" - }); - // Apply content if provided - if (typeof content == "string"){ - this.curtain.content_selector.html(content); - } - return this.curtain; - }.bind(this), - - /** + update: function (content, css) { + if (!this.curtain.showing) { + return this.curtain; + } + clearTimeout(this.curtain.hide_delay); + // Apply CSS if provided + if (typeof css == 'object') { + this.curtain.selector.style(css); + } + // Update size and position + var page_origin = this.getPageOrigin(); + this.curtain.selector.style({ + top: page_origin.y + 'px', + left: page_origin.x + 'px', + width: this.layout.width + 'px', + height: this.layout.height + 'px' + }); + this.curtain.content_selector.style({ + 'max-width': this.layout.width - 40 + 'px', + 'max-height': this.layout.height - 40 + 'px' + }); + // Apply content if provided + if (typeof content == 'string') { + this.curtain.content_selector.html(content); + } + return this.curtain; + }.bind(this), + /** * Remove the curtain * @param {number} delay Time to wait (in ms) */ - hide: function(delay){ - if (!this.curtain.showing){ return this.curtain; } - // If a delay was passed then defer to a timeout - if (typeof delay == "number"){ - clearTimeout(this.curtain.hide_delay); - this.curtain.hide_delay = setTimeout(this.curtain.hide, delay); - return this.curtain; - } - // Remove curtain - this.curtain.selector.remove(); - this.curtain.selector = null; - this.curtain.content_selector = null; - this.curtain.showing = false; - return this.curtain; - }.bind(this) - }; - return curtain; -}; - -/** + hide: function (delay) { + if (!this.curtain.showing) { + return this.curtain; + } + // If a delay was passed then defer to a timeout + if (typeof delay == 'number') { + clearTimeout(this.curtain.hide_delay); + this.curtain.hide_delay = setTimeout(this.curtain.hide, delay); + return this.curtain; + } + // Remove curtain + this.curtain.selector.remove(); + this.curtain.selector = null; + this.curtain.content_selector = null; + this.curtain.showing = false; + return this.curtain; + }.bind(this) + }; + return curtain; + }; + /** * Generate a loader object for a plot, panel, or any other subdivision of a layout * * The panel loader is a small HTML overlay that appears in the lower left corner of the panel. It cannot be styled @@ -630,121 +643,116 @@ LocusZoom.generateCurtain = function(){ * TODO Improve type documentation * @returns {object} */ -LocusZoom.generateLoader = function(){ - var loader = { - showing: false, - selector: null, - content_selector: null, - progress_selector: null, - cancel_selector: null, - - /** + LocusZoom.generateLoader = function () { + var loader = { + showing: false, + selector: null, + content_selector: null, + progress_selector: null, + cancel_selector: null, + /** * Show a loading indicator * @param {string} [content='Loading...'] Loading message (displayed as raw HTML) */ - show: function(content){ - // Generate loader - if (!this.loader.showing){ - this.loader.selector = d3.select(this.parent_plot.svg.node().parentNode).insert("div") - .attr("class", "lz-loader").attr("id", this.id + ".loader"); - this.loader.content_selector = this.loader.selector.append("div") - .attr("class", "lz-loader-content"); - this.loader.progress_selector = this.loader.selector - .append("div").attr("class", "lz-loader-progress-container") - .append("div").attr("class", "lz-loader-progress"); - /* TODO: figure out how to make this cancel button work + show: function (content) { + // Generate loader + if (!this.loader.showing) { + this.loader.selector = d3.select(this.parent_plot.svg.node().parentNode).insert('div').attr('class', 'lz-loader').attr('id', this.id + '.loader'); + this.loader.content_selector = this.loader.selector.append('div').attr('class', 'lz-loader-content'); + this.loader.progress_selector = this.loader.selector.append('div').attr('class', 'lz-loader-progress-container').append('div').attr('class', 'lz-loader-progress'); + /* TODO: figure out how to make this cancel button work this.loader.cancel_selector = this.loader.selector.append("div") .attr("class", "lz-loader-cancel").html("Cancel") .on("click", function(){ this.loader.hide(); }.bind(this)); */ - this.loader.showing = true; - if (typeof content == "undefined"){ content = "Loading..."; } - } - return this.loader.update(content); - }.bind(this), - - /** + this.loader.showing = true; + if (typeof content == 'undefined') { + content = 'Loading...'; + } + } + return this.loader.update(content); + }.bind(this), + /** * Update the currently displayed loader and ensure the new content is positioned correctly. * @param {string} content The text to display (as raw HTML). If not a string, will be ignored. * @param {number} [percent] A number from 1-100. If a value is specified, it will stop all animations * in progress. */ - update: function(content, percent){ - if (!this.loader.showing){ return this.loader; } - clearTimeout(this.loader.hide_delay); - // Apply content if provided - if (typeof content == "string"){ - this.loader.content_selector.html(content); - } - // Update size and position - var padding = 6; // is there a better place to store/define this? - var page_origin = this.getPageOrigin(); - var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); - this.loader.selector.style({ - top: (page_origin.y + this.layout.height - loader_boundrect.height - padding) + "px", - left: (page_origin.x + padding) + "px" - }); - /* Uncomment this code when a functional cancel button can be shown + update: function (content, percent) { + if (!this.loader.showing) { + return this.loader; + } + clearTimeout(this.loader.hide_delay); + // Apply content if provided + if (typeof content == 'string') { + this.loader.content_selector.html(content); + } + // Update size and position + var padding = 6; + // is there a better place to store/define this? + var page_origin = this.getPageOrigin(); + var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); + this.loader.selector.style({ + top: page_origin.y + this.layout.height - loader_boundrect.height - padding + 'px', + left: page_origin.x + padding + 'px' + }); + /* Uncomment this code when a functional cancel button can be shown var cancel_boundrect = this.loader.cancel_selector.node().getBoundingClientRect(); this.loader.content_selector.style({ "padding-right": (cancel_boundrect.width + padding) + "px" }); */ - // Apply percent if provided - if (typeof percent == "number"){ - this.loader.progress_selector.style({ - width: (Math.min(Math.max(percent, 1), 100)) + "%" - }); - } - return this.loader; - }.bind(this), - - /** + // Apply percent if provided + if (typeof percent == 'number') { + this.loader.progress_selector.style({ width: Math.min(Math.max(percent, 1), 100) + '%' }); + } + return this.loader; + }.bind(this), + /** * Adds a class to the loading bar that makes it loop infinitely in a loading animation. Useful when exact * percent progress is not available. */ - animate: function(){ - this.loader.progress_selector.classed("lz-loader-progress-animated", true); - return this.loader; - }.bind(this), - - /** + animate: function () { + this.loader.progress_selector.classed('lz-loader-progress-animated', true); + return this.loader; + }.bind(this), + /** * Sets the loading bar in the loader to percentage width equal to the percent (number) value passed. Percents * will automatically be limited to a range of 1 to 100. Will stop all animations in progress. */ - setPercentCompleted: function(percent){ - this.loader.progress_selector.classed("lz-loader-progress-animated", false); - return this.loader.update(null, percent); - }.bind(this), - - /** + setPercentCompleted: function (percent) { + this.loader.progress_selector.classed('lz-loader-progress-animated', false); + return this.loader.update(null, percent); + }.bind(this), + /** * Remove the loader * @param {number} delay Time to wait (in ms) */ - hide: function(delay){ - if (!this.loader.showing){ return this.loader; } - // If a delay was passed then defer to a timeout - if (typeof delay == "number"){ - clearTimeout(this.loader.hide_delay); - this.loader.hide_delay = setTimeout(this.loader.hide, delay); - return this.loader; - } - // Remove loader - this.loader.selector.remove(); - this.loader.selector = null; - this.loader.content_selector = null; - this.loader.progress_selector = null; - this.loader.cancel_selector = null; - this.loader.showing = false; - return this.loader; - }.bind(this) - }; - return loader; -}; - -/** + hide: function (delay) { + if (!this.loader.showing) { + return this.loader; + } + // If a delay was passed then defer to a timeout + if (typeof delay == 'number') { + clearTimeout(this.loader.hide_delay); + this.loader.hide_delay = setTimeout(this.loader.hide, delay); + return this.loader; + } + // Remove loader + this.loader.selector.remove(); + this.loader.selector = null; + this.loader.content_selector = null; + this.loader.progress_selector = null; + this.loader.cancel_selector = null; + this.loader.showing = false; + return this.loader; + }.bind(this) + }; + return loader; + }; + /** * Create a new subclass following classical inheritance patterns. Some registry singletons use this internally to * enable code reuse and customization of known LZ core functionality. * @@ -754,29 +762,24 @@ LocusZoom.generateLoader = function(){ * just calls the parent constructor by default. Implementer must manage super calls when overriding the constructor. * @returns {Function} The constructor for the new child class */ -LocusZoom.subclass = function(parent, extra, new_constructor) { - if (typeof parent !== "function" ) { - throw "Parent must be a callable constructor"; - } - - extra = extra || {}; - var Sub = new_constructor || function() { - parent.apply(this, arguments); - }; - - Sub.prototype = Object.create(parent.prototype); - Object.keys(extra).forEach(function(k) { - Sub.prototype[k] = extra[k]; - }); - Sub.prototype.constructor = Sub; - - return Sub; -}; - -/* global LocusZoom */ -"use strict"; - -/** + LocusZoom.subclass = function (parent, extra, new_constructor) { + if (typeof parent !== 'function') { + throw 'Parent must be a callable constructor'; + } + extra = extra || {}; + var Sub = new_constructor || function () { + parent.apply(this, arguments); + }; + Sub.prototype = Object.create(parent.prototype); + Object.keys(extra).forEach(function (k) { + Sub.prototype[k] = extra[k]; + }); + Sub.prototype.constructor = Sub; + return Sub; + }; + /* global LocusZoom */ + 'use strict'; + /** * Manage known layouts for all parts of the LocusZoom plot * * This registry allows for layouts to be reused and customized many times on a page, using a common base pattern. @@ -784,114 +787,116 @@ LocusZoom.subclass = function(parent, extra, new_constructor) { * * @class */ -LocusZoom.Layouts = (function() { - var obj = {}; - var layouts = { - "plot": {}, - "panel": {}, - "data_layer": {}, - "dashboard": {}, - "tooltip": {} - }; - - /** + LocusZoom.Layouts = function () { + var obj = {}; + var layouts = { + 'plot': {}, + 'panel': {}, + 'data_layer': {}, + 'dashboard': {}, + 'tooltip': {} + }; + /** * Generate a layout configuration object * @param {('plot'|'panel'|'data_layer'|'dashboard'|'tooltip')} type The type of layout to retrieve * @param {string} name Identifier of the predefined layout within the specified type * @param {object} [modifications] Custom properties that override default settings for this layout * @returns {object} A JSON-serializable object representation */ - obj.get = function(type, name, modifications) { - if (typeof type != "string" || typeof name != "string") { - throw("invalid arguments passed to LocusZoom.Layouts.get, requires string (layout type) and string (layout name)"); - } else if (layouts[type][name]) { - // Get the base layout - var layout = LocusZoom.Layouts.merge(modifications || {}, layouts[type][name]); - // If "unnamespaced" is true then strike that from the layout and return the layout without namespacing - if (layout.unnamespaced){ - delete layout.unnamespaced; - return JSON.parse(JSON.stringify(layout)); - } - // Determine the default namespace for namespaced values - var default_namespace = ""; - if (typeof layout.namespace == "string"){ - default_namespace = layout.namespace; - } else if (typeof layout.namespace == "object" && Object.keys(layout.namespace).length){ - if (typeof layout.namespace.default != "undefined"){ - default_namespace = layout.namespace.default; - } else { - default_namespace = layout.namespace[Object.keys(layout.namespace)[0]].toString(); - } - } - default_namespace += default_namespace.length ? ":" : ""; - // Apply namespaces to layout, recursively - var applyNamespaces = function(element, namespace){ - if (namespace){ - if (typeof namespace == "string"){ - namespace = { default: namespace }; + obj.get = function (type, name, modifications) { + if (typeof type != 'string' || typeof name != 'string') { + throw 'invalid arguments passed to LocusZoom.Layouts.get, requires string (layout type) and string (layout name)'; + } else if (layouts[type][name]) { + // Get the base layout + var layout = LocusZoom.Layouts.merge(modifications || {}, layouts[type][name]); + // If "unnamespaced" is true then strike that from the layout and return the layout without namespacing + if (layout.unnamespaced) { + delete layout.unnamespaced; + return JSON.parse(JSON.stringify(layout)); } - } else { - namespace = { default: "" }; - } - if (typeof element == "string"){ - var re = /\{\{namespace(\[[A-Za-z_0-9]+\]|)\}\}/g; - var match, base, key, resolved_namespace; - var replace = []; - while ((match = re.exec(element)) !== null){ - base = match[0]; - key = match[1].length ? match[1].replace(/(\[|\])/g,"") : null; - resolved_namespace = default_namespace; - if (namespace != null && typeof namespace == "object" && typeof namespace[key] != "undefined"){ - resolved_namespace = namespace[key] + (namespace[key].length ? ":" : ""); - } - replace.push({ base: base, namespace: resolved_namespace }); - } - for (var r in replace){ - element = element.replace(replace[r].base, replace[r].namespace); - } - } else if (typeof element == "object" && element != null){ - if (typeof element.namespace != "undefined"){ - var merge_namespace = (typeof element.namespace == "string") ? { default: element.namespace } : element.namespace; - namespace = LocusZoom.Layouts.merge(namespace, merge_namespace); - } - var namespaced_element, namespaced_property; - for (var property in element) { - if (property === "namespace"){ continue; } - namespaced_element = applyNamespaces(element[property], namespace); - namespaced_property = applyNamespaces(property, namespace); - if (property !== namespaced_property){ - delete element[property]; + // Determine the default namespace for namespaced values + var default_namespace = ''; + if (typeof layout.namespace == 'string') { + default_namespace = layout.namespace; + } else if (typeof layout.namespace == 'object' && Object.keys(layout.namespace).length) { + if (typeof layout.namespace.default != 'undefined') { + default_namespace = layout.namespace.default; + } else { + default_namespace = layout.namespace[Object.keys(layout.namespace)[0]].toString(); } - element[namespaced_property] = namespaced_element; } - } - return element; - }; - layout = applyNamespaces(layout, layout.namespace); - // Return the layout as valid JSON only - return JSON.parse(JSON.stringify(layout)); - } else { - throw("layout type [" + type + "] name [" + name + "] not found"); - } - }; - - /** @private */ - obj.set = function(type, name, layout) { - if (typeof type != "string" || typeof name != "string" || typeof layout != "object"){ - throw ("unable to set new layout; bad arguments passed to set()"); - } - if (!layouts[type]){ - layouts[type] = {}; - } - if (layout){ - return (layouts[type][name] = JSON.parse(JSON.stringify(layout))); - } else { - delete layouts[type][name]; - return null; - } - }; - - /** + default_namespace += default_namespace.length ? ':' : ''; + // Apply namespaces to layout, recursively + var applyNamespaces = function (element, namespace) { + if (namespace) { + if (typeof namespace == 'string') { + namespace = { default: namespace }; + } + } else { + namespace = { default: '' }; + } + if (typeof element == 'string') { + var re = /\{\{namespace(\[[A-Za-z_0-9]+\]|)\}\}/g; + var match, base, key, resolved_namespace; + var replace = []; + while ((match = re.exec(element)) !== null) { + base = match[0]; + key = match[1].length ? match[1].replace(/(\[|\])/g, '') : null; + resolved_namespace = default_namespace; + if (namespace != null && typeof namespace == 'object' && typeof namespace[key] != 'undefined') { + resolved_namespace = namespace[key] + (namespace[key].length ? ':' : ''); + } + replace.push({ + base: base, + namespace: resolved_namespace + }); + } + for (var r in replace) { + element = element.replace(replace[r].base, replace[r].namespace); + } + } else if (typeof element == 'object' && element != null) { + if (typeof element.namespace != 'undefined') { + var merge_namespace = typeof element.namespace == 'string' ? { default: element.namespace } : element.namespace; + namespace = LocusZoom.Layouts.merge(namespace, merge_namespace); + } + var namespaced_element, namespaced_property; + for (var property in element) { + if (property === 'namespace') { + continue; + } + namespaced_element = applyNamespaces(element[property], namespace); + namespaced_property = applyNamespaces(property, namespace); + if (property !== namespaced_property) { + delete element[property]; + } + element[namespaced_property] = namespaced_element; + } + } + return element; + }; + layout = applyNamespaces(layout, layout.namespace); + // Return the layout as valid JSON only + return JSON.parse(JSON.stringify(layout)); + } else { + throw 'layout type [' + type + '] name [' + name + '] not found'; + } + }; + /** @private */ + obj.set = function (type, name, layout) { + if (typeof type != 'string' || typeof name != 'string' || typeof layout != 'object') { + throw 'unable to set new layout; bad arguments passed to set()'; + } + if (!layouts[type]) { + layouts[type] = {}; + } + if (layout) { + return layouts[type][name] = JSON.parse(JSON.stringify(layout)); + } else { + delete layouts[type][name]; + return null; + } + }; + /** * Register a new layout definition by name. * * @param {string} type The type of layout to add. Usually, this will be one of the predefined LocusZoom types, @@ -900,28 +905,26 @@ LocusZoom.Layouts = (function() { * @param {object} [layout] A JSON-serializable object containing configuration properties for this layout * @returns The JSON representation of the newly created layout */ - obj.add = function(type, name, layout) { - return obj.set(type, name, layout); - }; - - /** + obj.add = function (type, name, layout) { + return obj.set(type, name, layout); + }; + /** * List all registered layouts * @param [type] Optionally narrow the list to only layouts of a specific type; else return all known layouts * @returns {*} */ - obj.list = function(type) { - if (!layouts[type]){ - var list = {}; - Object.keys(layouts).forEach(function(type){ - list[type] = Object.keys(layouts[type]); - }); - return list; - } else { - return Object.keys(layouts[type]); - } - }; - - /** + obj.list = function (type) { + if (!layouts[type]) { + var list = {}; + Object.keys(layouts).forEach(function (type) { + list[type] = Object.keys(layouts[type]); + }); + return list; + } else { + return Object.keys(layouts[type]); + } + }; + /** * A helper method used for merging two objects. If a key is present in both, takes the value from the first object * Values from `default_layout` will be cleanly copied over, ensuring no references or shared state. * @@ -931,1072 +934,1337 @@ LocusZoom.Layouts = (function() { * @param {object} default_layout An object containing default settings. * @returns The custom layout is modified in place and also returned from this method. */ - obj.merge = function (custom_layout, default_layout) { - if (typeof custom_layout !== "object" || typeof default_layout !== "object"){ - throw("LocusZoom.Layouts.merge only accepts two layout objects; " + (typeof custom_layout) + ", " + (typeof default_layout) + " given"); - } - for (var property in default_layout) { - if (!default_layout.hasOwnProperty(property)){ continue; } - // Get types for comparison. Treat nulls in the custom layout as undefined for simplicity. - // (javascript treats nulls as "object" when we just want to overwrite them as if they're undefined) - // Also separate arrays from objects as a discrete type. - var custom_type = custom_layout[property] === null ? "undefined" : typeof custom_layout[property]; - var default_type = typeof default_layout[property]; - if (custom_type === "object" && Array.isArray(custom_layout[property])){ custom_type = "array"; } - if (default_type === "object" && Array.isArray(default_layout[property])){ default_type = "array"; } - // Unsupported property types: throw an exception - if (custom_type === "function" || default_type === "function"){ - throw("LocusZoom.Layouts.merge encountered an unsupported property type"); - } - // Undefined custom value: pull the default value - if (custom_type === "undefined"){ - custom_layout[property] = JSON.parse(JSON.stringify(default_layout[property])); - continue; - } - // Both values are objects: merge recursively - if (custom_type === "object" && default_type === "object"){ - custom_layout[property] = LocusZoom.Layouts.merge(custom_layout[property], default_layout[property]); - continue; - } - } - return custom_layout; - }; - - return obj; -})(); - - -/** + obj.merge = function (custom_layout, default_layout) { + if (typeof custom_layout !== 'object' || typeof default_layout !== 'object') { + throw 'LocusZoom.Layouts.merge only accepts two layout objects; ' + typeof custom_layout + ', ' + typeof default_layout + ' given'; + } + for (var property in default_layout) { + if (!default_layout.hasOwnProperty(property)) { + continue; + } + // Get types for comparison. Treat nulls in the custom layout as undefined for simplicity. + // (javascript treats nulls as "object" when we just want to overwrite them as if they're undefined) + // Also separate arrays from objects as a discrete type. + var custom_type = custom_layout[property] === null ? 'undefined' : typeof custom_layout[property]; + var default_type = typeof default_layout[property]; + if (custom_type === 'object' && Array.isArray(custom_layout[property])) { + custom_type = 'array'; + } + if (default_type === 'object' && Array.isArray(default_layout[property])) { + default_type = 'array'; + } + // Unsupported property types: throw an exception + if (custom_type === 'function' || default_type === 'function') { + throw 'LocusZoom.Layouts.merge encountered an unsupported property type'; + } + // Undefined custom value: pull the default value + if (custom_type === 'undefined') { + custom_layout[property] = JSON.parse(JSON.stringify(default_layout[property])); + continue; + } + // Both values are objects: merge recursively + if (custom_type === 'object' && default_type === 'object') { + custom_layout[property] = LocusZoom.Layouts.merge(custom_layout[property], default_layout[property]); + continue; + } + } + return custom_layout; + }; + return obj; + }(); + /** * Tooltip Layouts * @namespace LocusZoom.Layouts.tooltips */ - -// TODO: Improve documentation of predefined types within layout namespaces -LocusZoom.Layouts.add("tooltip", "standard_association", { - namespace: { "assoc": "assoc" }, - closable: true, - show: { or: ["highlighted", "selected"] }, - hide: { and: ["unhighlighted", "unselected"] }, - html: "{{{{namespace[assoc]}}variant}}
" - + "P Value: {{{{namespace[assoc]}}log_pvalue|logtoscinotation}}
" - + "Ref. Allele: {{{{namespace[assoc]}}ref_allele}}
" - + "Make LD Reference
" -}); - -var covariates_model_association = LocusZoom.Layouts.get("tooltip", "standard_association", { unnamespaced: true }); -covariates_model_association.html += "Condition on Variant
"; -LocusZoom.Layouts.add("tooltip", "covariates_model_association", covariates_model_association); - -LocusZoom.Layouts.add("tooltip", "standard_genes", { - closable: true, - show: { or: ["highlighted", "selected"] }, - hide: { and: ["unhighlighted", "unselected"] }, - html: "

{{gene_name}}

" - + "
Gene ID: {{gene_id}}
" - + "
Transcript ID: {{transcript_id}}
" - + "
" - + "" - + "" - + "" - + "" - + "" - + "
ConstraintExpected variantsObserved variantsConst. Metric
Synonymous{{exp_syn}}{{n_syn}}z = {{syn_z}}
Missense{{exp_mis}}{{n_mis}}z = {{mis_z}}
LoF{{exp_lof}}{{n_lof}}pLI = {{pLI}}
" - + "More data on ExAC" -}); - -LocusZoom.Layouts.add("tooltip", "standard_intervals", { - namespace: { "intervals": "intervals" }, - closable: false, - show: { or: ["highlighted", "selected"] }, - hide: { and: ["unhighlighted", "unselected"] }, - html: "{{{{namespace[intervals]}}state_name}}
{{{{namespace[intervals]}}start}}-{{{{namespace[intervals]}}end}}" -}); - -/** + // TODO: Improve documentation of predefined types within layout namespaces + LocusZoom.Layouts.add('tooltip', 'standard_association', { + namespace: { 'assoc': 'assoc' }, + closable: true, + show: { + or: [ + 'highlighted', + 'selected' + ] + }, + hide: { + and: [ + 'unhighlighted', + 'unselected' + ] + }, + html: '{{{{namespace[assoc]}}variant}}
' + 'P Value: {{{{namespace[assoc]}}log_pvalue|logtoscinotation}}
' + 'Ref. Allele: {{{{namespace[assoc]}}ref_allele}}
' + 'Make LD Reference
' + }); + var covariates_model_association = LocusZoom.Layouts.get('tooltip', 'standard_association', { unnamespaced: true }); + covariates_model_association.html += 'Condition on Variant
'; + LocusZoom.Layouts.add('tooltip', 'covariates_model_association', covariates_model_association); + LocusZoom.Layouts.add('tooltip', 'standard_genes', { + closable: true, + show: { + or: [ + 'highlighted', + 'selected' + ] + }, + hide: { + and: [ + 'unhighlighted', + 'unselected' + ] + }, + html: '

{{gene_name}}

' + '
Gene ID: {{gene_id}}
' + '
Transcript ID: {{transcript_id}}
' + '
' + '' + '' + '' + '' + '' + '
ConstraintExpected variantsObserved variantsConst. Metric
Synonymous{{exp_syn}}{{n_syn}}z = {{syn_z}}
Missense{{exp_mis}}{{n_mis}}z = {{mis_z}}
LoF{{exp_lof}}{{n_lof}}pLI = {{pLI}}
' + 'More data on ExAC' + }); + LocusZoom.Layouts.add('tooltip', 'standard_intervals', { + namespace: { 'intervals': 'intervals' }, + closable: false, + show: { + or: [ + 'highlighted', + 'selected' + ] + }, + hide: { + and: [ + 'unhighlighted', + 'unselected' + ] + }, + html: '{{{{namespace[intervals]}}state_name}}
{{{{namespace[intervals]}}start}}-{{{{namespace[intervals]}}end}}' + }); + /** * Data Layer Layouts: represent specific information from a data source * @namespace Layouts.data_layer */ - -LocusZoom.Layouts.add("data_layer", "significance", { - id: "significance", - type: "orthogonal_line", - orientation: "horizontal", - offset: 4.522 -}); - -LocusZoom.Layouts.add("data_layer", "recomb_rate", { - namespace: { "recomb": "recomb" }, - id: "recombrate", - type: "line", - fields: ["{{namespace[recomb]}}position", "{{namespace[recomb]}}recomb_rate"], - z_index: 1, - style: { - "stroke": "#0000FF", - "stroke-width": "1.5px" - }, - x_axis: { - field: "{{namespace[recomb]}}position" - }, - y_axis: { - axis: 2, - field: "{{namespace[recomb]}}recomb_rate", - floor: 0, - ceiling: 100 - } -}); - -LocusZoom.Layouts.add("data_layer", "association_pvalues", { - namespace: { "assoc": "assoc", "ld": "ld" }, - id: "associationpvalues", - type: "scatter", - point_shape: { - scale_function: "if", - field: "{{namespace[ld]}}isrefvar", - parameters: { - field_value: 1, - then: "diamond", - else: "circle" - } - }, - point_size: { - scale_function: "if", - field: "{{namespace[ld]}}isrefvar", - parameters: { - field_value: 1, - then: 80, - else: 40 - } - }, - color: [ - { - scale_function: "if", - field: "{{namespace[ld]}}isrefvar", - parameters: { - field_value: 1, - then: "#9632b8" - } - }, - { - scale_function: "numerical_bin", - field: "{{namespace[ld]}}state", - parameters: { - breaks: [0, 0.2, 0.4, 0.6, 0.8], - values: ["#357ebd","#46b8da","#5cb85c","#eea236","#d43f3a"] - } - }, - "#B8B8B8" - ], - legend: [ - { shape: "diamond", color: "#9632b8", size: 40, label: "LD Ref Var", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#d43f3a", size: 40, label: "1.0 > r² ≥ 0.8", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#eea236", size: 40, label: "0.8 > r² ≥ 0.6", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#5cb85c", size: 40, label: "0.6 > r² ≥ 0.4", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#46b8da", size: 40, label: "0.4 > r² ≥ 0.2", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#357ebd", size: 40, label: "0.2 > r² ≥ 0.0", class: "lz-data_layer-scatter" }, - { shape: "circle", color: "#B8B8B8", size: 40, label: "no r² data", class: "lz-data_layer-scatter" } - ], - fields: ["{{namespace[assoc]}}variant", "{{namespace[assoc]}}position", "{{namespace[assoc]}}log_pvalue", "{{namespace[assoc]}}log_pvalue|logtoscinotation", "{{namespace[assoc]}}ref_allele", "{{namespace[ld]}}state", "{{namespace[ld]}}isrefvar"], - id_field: "{{namespace[assoc]}}variant", - z_index: 2, - x_axis: { - field: "{{namespace[assoc]}}position" - }, - y_axis: { - axis: 1, - field: "{{namespace[assoc]}}log_pvalue", - floor: 0, - upper_buffer: 0.10, - min_extent: [ 0, 10 ] - }, - behaviors: { - onmouseover: [ - { action: "set", status: "highlighted" } - ], - onmouseout: [ - { action: "unset", status: "highlighted" } - ], - onclick: [ - { action: "toggle", status: "selected", exclusive: true } - ], - onshiftclick: [ - { action: "toggle", status: "selected" } - ] - }, - tooltip: LocusZoom.Layouts.get("tooltip", "standard_association", { unnamespaced: true }) -}); - -LocusZoom.Layouts.add("data_layer", "phewas_pvalues", { - namespace: {"phewas": "phewas"}, - id: "phewaspvalues", - type: "category_scatter", - point_shape: "circle", - point_size: 70, - tooltip_positioning: "vertical", - id_field: "{{namespace[phewas]}}id", - fields: ["{{namespace[phewas]}}id", "{{namespace[phewas]}}log_pvalue", "{{namespace[phewas]}}trait_group", "{{namespace[phewas]}}trait_label"], - x_axis: { - field: "{{namespace[phewas]}}x", // Synthetic/derived field added by `category_scatter` layer - category_field: "{{namespace[phewas]}}trait_group" - }, - y_axis: { - axis: 1, - field: "{{namespace[phewas]}}log_pvalue", - floor: 0, - upper_buffer: 0.15 - }, - color: { - field: "{{namespace[phewas]}}trait_group", - scale_function: "categorical_bin", - parameters: { - categories: [], - values: [], - null_value: "#B8B8B8" - } - }, - fill_opacity: 0.7, - tooltip: { - closable: true, - show: { or: ["highlighted", "selected"] }, - hide: { and: ["unhighlighted", "unselected"] }, - html: [ - "Trait: {{{{namespace[phewas]}}trait_label|htmlescape}}
", - "Trait Category: {{{{namespace[phewas]}}trait_group|htmlescape}}
", - "P-value: {{{{namespace[phewas]}}log_pvalue|logtoscinotation|htmlescape}}
" - ].join("") - }, - behaviors: { - onmouseover: [ - { action: "set", status: "highlighted" } - ], - onmouseout: [ - { action: "unset", status: "highlighted" } - ], - onclick: [ - { action: "toggle", status: "selected", exclusive: true } - ], - onshiftclick: [ - { action: "toggle", status: "selected" } - ] - }, - label: { - text: "{{{{namespace[phewas]}}trait_label}}", - spacing: 6, - lines: { - style: { - "stroke-width": "2px", - "stroke": "#333333", - "stroke-dasharray": "2px 2px" - } - }, - filters: [ - { - field: "{{namespace[phewas]}}log_pvalue", - operator: ">=", - value: 20 - } - ], - style: { - "font-size": "14px", - "font-weight": "bold", - "fill": "#333333" - } - } -}); - -LocusZoom.Layouts.add("data_layer", "genes", { - namespace: { "gene": "gene", "constraint": "constraint" }, - id: "genes", - type: "genes", - fields: ["{{namespace[gene]}}gene", "{{namespace[constraint]}}constraint"], - id_field: "gene_id", - behaviors: { - onmouseover: [ - { action: "set", status: "highlighted" } - ], - onmouseout: [ - { action: "unset", status: "highlighted" } - ], - onclick: [ - { action: "toggle", status: "selected", exclusive: true } - ], - onshiftclick: [ - { action: "toggle", status: "selected" } - ] - }, - tooltip: LocusZoom.Layouts.get("tooltip", "standard_genes", { unnamespaced: true }) -}); - -LocusZoom.Layouts.add("data_layer", "genome_legend", { - namespace: { "genome": "genome" }, - id: "genome_legend", - type: "genome_legend", - fields: ["{{namespace[genome]}}chr", "{{namespace[genome]}}base_pairs"], - x_axis: { - floor: 0, - ceiling: 2881033286 - } -}); - -LocusZoom.Layouts.add("data_layer", "intervals", { - namespace: { "intervals": "intervals" }, - id: "intervals", - type: "intervals", - fields: ["{{namespace[intervals]}}start","{{namespace[intervals]}}end","{{namespace[intervals]}}state_id","{{namespace[intervals]}}state_name"], - id_field: "{{namespace[intervals]}}start", - start_field: "{{namespace[intervals]}}start", - end_field: "{{namespace[intervals]}}end", - track_split_field: "{{namespace[intervals]}}state_id", - split_tracks: true, - always_hide_legend: false, - color: { - field: "{{namespace[intervals]}}state_id", - scale_function: "categorical_bin", - parameters: { - categories: [1,2,3,4,5,6,7,8,9,10,11,12,13], - values: ["rgb(212,63,58)", "rgb(250,120,105)", "rgb(252,168,139)", "rgb(240,189,66)", "rgb(250,224,105)", "rgb(240,238,84)", "rgb(244,252,23)", "rgb(23,232,252)", "rgb(32,191,17)", "rgb(23,166,77)", "rgb(32,191,17)", "rgb(162,133,166)", "rgb(212,212,212)"], - null_value: "#B8B8B8" - } - }, - legend: [ - { shape: "rect", color: "rgb(212,63,58)", width: 9, label: "Active Promoter", "{{namespace[intervals]}}state_id": 1 }, - { shape: "rect", color: "rgb(250,120,105)", width: 9, label: "Weak Promoter", "{{namespace[intervals]}}state_id": 2 }, - { shape: "rect", color: "rgb(252,168,139)", width: 9, label: "Poised Promoter", "{{namespace[intervals]}}state_id": 3 }, - { shape: "rect", color: "rgb(240,189,66)", width: 9, label: "Strong enhancer", "{{namespace[intervals]}}state_id": 4 }, - { shape: "rect", color: "rgb(250,224,105)", width: 9, label: "Strong enhancer", "{{namespace[intervals]}}state_id": 5 }, - { shape: "rect", color: "rgb(240,238,84)", width: 9, label: "Weak enhancer", "{{namespace[intervals]}}state_id": 6 }, - { shape: "rect", color: "rgb(244,252,23)", width: 9, label: "Weak enhancer", "{{namespace[intervals]}}state_id": 7 }, - { shape: "rect", color: "rgb(23,232,252)", width: 9, label: "Insulator", "{{namespace[intervals]}}state_id": 8 }, - { shape: "rect", color: "rgb(32,191,17)", width: 9, label: "Transcriptional transition", "{{namespace[intervals]}}state_id": 9 }, - { shape: "rect", color: "rgb(23,166,77)", width: 9, label: "Transcriptional elongation", "{{namespace[intervals]}}state_id": 10 }, - { shape: "rect", color: "rgb(136,240,129)", width: 9, label: "Weak transcribed", "{{namespace[intervals]}}state_id": 11 }, - { shape: "rect", color: "rgb(162,133,166)", width: 9, label: "Polycomb-repressed", "{{namespace[intervals]}}state_id": 12 }, - { shape: "rect", color: "rgb(212,212,212)", width: 9, label: "Heterochromatin / low signal", "{{namespace[intervals]}}state_id": 13 } - ], - behaviors: { - onmouseover: [ - { action: "set", status: "highlighted" } - ], - onmouseout: [ - { action: "unset", status: "highlighted" } - ], - onclick: [ - { action: "toggle", status: "selected", exclusive: true } - ], - onshiftclick: [ - { action: "toggle", status: "selected" } - ] - }, - tooltip: LocusZoom.Layouts.get("tooltip", "standard_intervals", { unnamespaced: true }) -}); - -/** - * Dashboard Layouts: toolbar buttons etc - * @namespace Layouts.dashboard - */ -LocusZoom.Layouts.add("dashboard", "standard_panel", { - components: [ - { - type: "remove_panel", - position: "right", - color: "red", - group_position: "end" - }, - { - type: "move_panel_up", - position: "right", - group_position: "middle" - }, - { - type: "move_panel_down", - position: "right", - group_position: "start", - style: { "margin-left": "0.75em" } - } - ] -}); - -LocusZoom.Layouts.add("dashboard", "standard_plot", { - components: [ - { - type: "title", - title: "LocusZoom", - subtitle: "v" + LocusZoom.version + "", - position: "left" - }, - { - type: "dimensions", - position: "right" - }, - { - type: "region_scale", - position: "right" - }, - { - type: "download", - position: "right" - } - ] -}); - -var covariates_model_plot_dashboard = LocusZoom.Layouts.get("dashboard", "standard_plot"); -covariates_model_plot_dashboard.components.push({ - type: "covariates_model", - button_html: "Model", - button_title: "Show and edit covariates currently in model", - position: "left" -}); -LocusZoom.Layouts.add("dashboard", "covariates_model_plot", covariates_model_plot_dashboard); - -var region_nav_plot_dashboard = LocusZoom.Layouts.get("dashboard", "standard_plot"); -region_nav_plot_dashboard.components.push({ - type: "shift_region", - step: 500000, - button_html: ">>", - position: "right", - group_position: "end" -}); -region_nav_plot_dashboard.components.push({ - type: "shift_region", - step: 50000, - button_html: ">", - position: "right", - group_position: "middle" -}); -region_nav_plot_dashboard.components.push({ - type: "zoom_region", - step: 0.2, - position: "right", - group_position: "middle" -}); -region_nav_plot_dashboard.components.push({ - type: "zoom_region", - step: -0.2, - position: "right", - group_position: "middle" -}); -region_nav_plot_dashboard.components.push({ - type: "shift_region", - step: -50000, - button_html: "<", - position: "right", - group_position: "middle" -}); -region_nav_plot_dashboard.components.push({ - type: "shift_region", - step: -500000, - button_html: "<<", - position: "right", - group_position: "start" -}); -LocusZoom.Layouts.add("dashboard", "region_nav_plot", region_nav_plot_dashboard); - -/** - * Panel Layouts - * @namespace Layouts.panel - */ - -LocusZoom.Layouts.add("panel", "association", { - id: "association", - width: 800, - height: 225, - min_width: 400, - min_height: 200, - proportional_width: 1, - margin: { top: 35, right: 50, bottom: 40, left: 50 }, - inner_border: "rgb(210, 210, 210)", - dashboard: (function(){ - var l = LocusZoom.Layouts.get("dashboard", "standard_panel", { unnamespaced: true }); - l.components.push({ - type: "toggle_legend", - position: "right" + LocusZoom.Layouts.add('data_layer', 'significance', { + id: 'significance', + type: 'orthogonal_line', + orientation: 'horizontal', + offset: 4.522 }); - return l; - })(), - axes: { - x: { - label: "Chromosome {{chr}} (Mb)", - label_offset: 32, - tick_format: "region", - extent: "state" - }, - y1: { - label: "-log10 p-value", - label_offset: 28 - }, - y2: { - label: "Recombination Rate (cM/Mb)", - label_offset: 40 - } - }, - legend: { - orientation: "vertical", - origin: { x: 55, y: 40 }, - hidden: true - }, - interaction: { - drag_background_to_pan: true, - drag_x_ticks_to_scale: true, - drag_y1_ticks_to_scale: true, - drag_y2_ticks_to_scale: true, - scroll_to_zoom: true, - x_linked: true - }, - data_layers: [ - LocusZoom.Layouts.get("data_layer", "significance", { unnamespaced: true }), - LocusZoom.Layouts.get("data_layer", "recomb_rate", { unnamespaced: true }), - LocusZoom.Layouts.get("data_layer", "association_pvalues", { unnamespaced: true }) - ] -}); - -LocusZoom.Layouts.add("panel", "genes", { - id: "genes", - width: 800, - height: 225, - min_width: 400, - min_height: 112.5, - proportional_width: 1, - margin: { top: 20, right: 50, bottom: 20, left: 50 }, - axes: {}, - interaction: { - drag_background_to_pan: true, - scroll_to_zoom: true, - x_linked: true - }, - dashboard: (function(){ - var l = LocusZoom.Layouts.get("dashboard", "standard_panel", { unnamespaced: true }); - l.components.push({ - type: "resize_to_data", - position: "right" + LocusZoom.Layouts.add('data_layer', 'recomb_rate', { + namespace: { 'recomb': 'recomb' }, + id: 'recombrate', + type: 'line', + fields: [ + '{{namespace[recomb]}}position', + '{{namespace[recomb]}}recomb_rate' + ], + z_index: 1, + style: { + 'stroke': '#0000FF', + 'stroke-width': '1.5px' + }, + x_axis: { field: '{{namespace[recomb]}}position' }, + y_axis: { + axis: 2, + field: '{{namespace[recomb]}}recomb_rate', + floor: 0, + ceiling: 100 + } }); - return l; - })(), - data_layers: [ - LocusZoom.Layouts.get("data_layer", "genes", { unnamespaced: true }) - ] -}); - -LocusZoom.Layouts.add("panel", "phewas", { - id: "phewas", - width: 800, - height: 300, - min_width: 800, - min_height: 300, - proportional_width: 1, - margin: { top: 20, right: 50, bottom: 120, left: 50 }, - inner_border: "rgb(210, 210, 210)", - axes: { - x: { - ticks: { // Object based config (shared defaults; allow layers to specify ticks) - style: { - "font-weight": "bold", - "font-size": "11px", - "text-anchor": "start" + LocusZoom.Layouts.add('data_layer', 'association_pvalues', { + namespace: { + 'assoc': 'assoc', + 'ld': 'ld' + }, + id: 'associationpvalues', + type: 'scatter', + point_shape: { + scale_function: 'if', + field: '{{namespace[ld]}}isrefvar', + parameters: { + field_value: 1, + then: 'diamond', + else: 'circle' + } + }, + point_size: { + scale_function: 'if', + field: '{{namespace[ld]}}isrefvar', + parameters: { + field_value: 1, + then: 80, + else: 40 + } + }, + color: [ + { + scale_function: 'if', + field: '{{namespace[ld]}}isrefvar', + parameters: { + field_value: 1, + then: '#9632b8' + } }, - transform: "rotate(50)", - position: "left" // Special param recognized by `category_scatter` layers - } - }, - y1: { - label: "-log10 p-value", - label_offset: 28 - } - }, - data_layers: [ - LocusZoom.Layouts.get("data_layer", "significance", { unnamespaced: true }), - LocusZoom.Layouts.get("data_layer", "phewas_pvalues", { unnamespaced: true }) - ] -}); - -LocusZoom.Layouts.add("panel", "genome_legend", { - id: "genome_legend", - width: 800, - height: 50, - origin: { x: 0, y: 300 }, - min_width: 800, - min_height: 50, - proportional_width: 1, - margin: { top: 0, right: 50, bottom: 35, left: 50 }, - axes: { - x: { - label: "Genomic Position (number denotes chromosome)", - label_offset: 35, - ticks: [ { - x: 124625310, - text: "1", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + scale_function: 'numerical_bin', + field: '{{namespace[ld]}}state', + parameters: { + breaks: [ + 0, + 0.2, + 0.4, + 0.6, + 0.8 + ], + values: [ + '#357ebd', + '#46b8da', + '#5cb85c', + '#eea236', + '#d43f3a' + ] + } }, + '#B8B8B8' + ], + legend: [ { - x: 370850307, - text: "2", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'diamond', + color: '#9632b8', + size: 40, + label: 'LD Ref Var', + class: 'lz-data_layer-scatter' }, { - x: 591461209, - text: "3", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'circle', + color: '#d43f3a', + size: 40, + label: '1.0 > r\xB2 \u2265 0.8', + class: 'lz-data_layer-scatter' }, { - x: 786049562, - text: "4", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'circle', + color: '#eea236', + size: 40, + label: '0.8 > r\xB2 \u2265 0.6', + class: 'lz-data_layer-scatter' }, { - x: 972084330, - text: "5", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'circle', + color: '#5cb85c', + size: 40, + label: '0.6 > r\xB2 \u2265 0.4', + class: 'lz-data_layer-scatter' }, { - x: 1148099493, - text: "6", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'circle', + color: '#46b8da', + size: 40, + label: '0.4 > r\xB2 \u2265 0.2', + class: 'lz-data_layer-scatter' }, { - x: 1313226358, - text: "7", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'circle', + color: '#357ebd', + size: 40, + label: '0.2 > r\xB2 \u2265 0.0', + class: 'lz-data_layer-scatter' }, { - x: 1465977701, - text: "8", + shape: 'circle', + color: '#B8B8B8', + size: 40, + label: 'no r\xB2 data', + class: 'lz-data_layer-scatter' + } + ], + fields: [ + '{{namespace[assoc]}}variant', + '{{namespace[assoc]}}position', + '{{namespace[assoc]}}log_pvalue', + '{{namespace[assoc]}}log_pvalue|logtoscinotation', + '{{namespace[assoc]}}ref_allele', + '{{namespace[ld]}}state', + '{{namespace[ld]}}isrefvar' + ], + id_field: '{{namespace[assoc]}}variant', + z_index: 2, + x_axis: { field: '{{namespace[assoc]}}position' }, + y_axis: { + axis: 1, + field: '{{namespace[assoc]}}log_pvalue', + floor: 0, + upper_buffer: 0.1, + min_extent: [ + 0, + 10 + ] + }, + behaviors: { + onmouseover: [{ + action: 'set', + status: 'highlighted' + }], + onmouseout: [{ + action: 'unset', + status: 'highlighted' + }], + onclick: [{ + action: 'toggle', + status: 'selected', + exclusive: true + }], + onshiftclick: [{ + action: 'toggle', + status: 'selected' + }] + }, + tooltip: LocusZoom.Layouts.get('tooltip', 'standard_association', { unnamespaced: true }) + }); + LocusZoom.Layouts.add('data_layer', 'phewas_pvalues', { + namespace: { 'phewas': 'phewas' }, + id: 'phewaspvalues', + type: 'category_scatter', + point_shape: 'circle', + point_size: 70, + tooltip_positioning: 'vertical', + id_field: '{{namespace[phewas]}}id', + fields: [ + '{{namespace[phewas]}}id', + '{{namespace[phewas]}}log_pvalue', + '{{namespace[phewas]}}trait_group', + '{{namespace[phewas]}}trait_label' + ], + x_axis: { + field: '{{namespace[phewas]}}x', + // Synthetic/derived field added by `category_scatter` layer + category_field: '{{namespace[phewas]}}trait_group', + lower_buffer: 0.025, + upper_buffer: 0.025 + }, + y_axis: { + axis: 1, + field: '{{namespace[phewas]}}log_pvalue', + floor: 0, + upper_buffer: 0.15 + }, + color: { + field: '{{namespace[phewas]}}trait_group', + scale_function: 'categorical_bin', + parameters: { + categories: [], + values: [], + null_value: '#B8B8B8' + } + }, + fill_opacity: 0.7, + tooltip: { + closable: true, + show: { + or: [ + 'highlighted', + 'selected' + ] + }, + hide: { + and: [ + 'unhighlighted', + 'unselected' + ] + }, + html: [ + 'Trait: {{{{namespace[phewas]}}trait_label|htmlescape}}
', + 'Trait Category: {{{{namespace[phewas]}}trait_group|htmlescape}}
', + 'P-value: {{{{namespace[phewas]}}log_pvalue|logtoscinotation|htmlescape}}
' + ].join('') + }, + behaviors: { + onmouseover: [{ + action: 'set', + status: 'highlighted' + }], + onmouseout: [{ + action: 'unset', + status: 'highlighted' + }], + onclick: [{ + action: 'toggle', + status: 'selected', + exclusive: true + }], + onshiftclick: [{ + action: 'toggle', + status: 'selected' + }] + }, + label: { + text: '{{{{namespace[phewas]}}trait_label}}', + spacing: 6, + lines: { style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + 'stroke-width': '2px', + 'stroke': '#333333', + 'stroke-dasharray': '2px 2px' + } }, + filters: [{ + field: '{{namespace[phewas]}}log_pvalue', + operator: '>=', + value: 20 + }], + style: { + 'font-size': '14px', + 'font-weight': 'bold', + 'fill': '#333333' + } + } + }); + LocusZoom.Layouts.add('data_layer', 'genes', { + namespace: { + 'gene': 'gene', + 'constraint': 'constraint' + }, + id: 'genes', + type: 'genes', + fields: [ + '{{namespace[gene]}}gene', + '{{namespace[constraint]}}constraint' + ], + id_field: 'gene_id', + behaviors: { + onmouseover: [{ + action: 'set', + status: 'highlighted' + }], + onmouseout: [{ + action: 'unset', + status: 'highlighted' + }], + onclick: [{ + action: 'toggle', + status: 'selected', + exclusive: true + }], + onshiftclick: [{ + action: 'toggle', + status: 'selected' + }] + }, + tooltip: LocusZoom.Layouts.get('tooltip', 'standard_genes', { unnamespaced: true }) + }); + LocusZoom.Layouts.add('data_layer', 'genome_legend', { + namespace: { 'genome': 'genome' }, + id: 'genome_legend', + type: 'genome_legend', + fields: [ + '{{namespace[genome]}}chr', + '{{namespace[genome]}}base_pairs' + ], + x_axis: { + floor: 0, + ceiling: 2881033286 + } + }); + LocusZoom.Layouts.add('data_layer', 'intervals', { + namespace: { 'intervals': 'intervals' }, + id: 'intervals', + type: 'intervals', + fields: [ + '{{namespace[intervals]}}start', + '{{namespace[intervals]}}end', + '{{namespace[intervals]}}state_id', + '{{namespace[intervals]}}state_name' + ], + id_field: '{{namespace[intervals]}}start', + start_field: '{{namespace[intervals]}}start', + end_field: '{{namespace[intervals]}}end', + track_split_field: '{{namespace[intervals]}}state_id', + split_tracks: true, + always_hide_legend: false, + color: { + field: '{{namespace[intervals]}}state_id', + scale_function: 'categorical_bin', + parameters: { + categories: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ], + values: [ + 'rgb(212,63,58)', + 'rgb(250,120,105)', + 'rgb(252,168,139)', + 'rgb(240,189,66)', + 'rgb(250,224,105)', + 'rgb(240,238,84)', + 'rgb(244,252,23)', + 'rgb(23,232,252)', + 'rgb(32,191,17)', + 'rgb(23,166,77)', + 'rgb(32,191,17)', + 'rgb(162,133,166)', + 'rgb(212,212,212)' + ], + null_value: '#B8B8B8' + } + }, + legend: [ { - x: 1609766427, - text: "9", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(212,63,58)', + width: 9, + label: 'Active Promoter', + '{{namespace[intervals]}}state_id': 1 }, { - x: 1748140516, - text: "10", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(250,120,105)', + width: 9, + label: 'Weak Promoter', + '{{namespace[intervals]}}state_id': 2 }, { - x: 1883411148, - text: "11", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(252,168,139)', + width: 9, + label: 'Poised Promoter', + '{{namespace[intervals]}}state_id': 3 }, { - x: 2017840353, - text: "12", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(240,189,66)', + width: 9, + label: 'Strong enhancer', + '{{namespace[intervals]}}state_id': 4 }, { - x: 2142351240, - text: "13", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(250,224,105)', + width: 9, + label: 'Strong enhancer', + '{{namespace[intervals]}}state_id': 5 }, { - x: 2253610949, - text: "14", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(240,238,84)', + width: 9, + label: 'Weak enhancer', + '{{namespace[intervals]}}state_id': 6 }, { - x: 2358551415, - text: "15", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(244,252,23)', + width: 9, + label: 'Weak enhancer', + '{{namespace[intervals]}}state_id': 7 }, { - x: 2454994487, - text: "16", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(23,232,252)', + width: 9, + label: 'Insulator', + '{{namespace[intervals]}}state_id': 8 }, { - x: 2540769469, - text: "17", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(32,191,17)', + width: 9, + label: 'Transcriptional transition', + '{{namespace[intervals]}}state_id': 9 }, { - x: 2620405698, - text: "18", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(23,166,77)', + width: 9, + label: 'Transcriptional elongation', + '{{namespace[intervals]}}state_id': 10 }, { - x: 2689008813, - text: "19", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(136,240,129)', + width: 9, + label: 'Weak transcribed', + '{{namespace[intervals]}}state_id': 11 }, { - x: 2750086065, - text: "20", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(162,133,166)', + width: 9, + label: 'Polycomb-repressed', + '{{namespace[intervals]}}state_id': 12 }, { - x: 2805663772, - text: "21", - style: { - "fill": "rgb(120, 120, 186)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + shape: 'rect', + color: 'rgb(212,212,212)', + width: 9, + label: 'Heterochromatin / low signal', + '{{namespace[intervals]}}state_id': 13 + } + ], + behaviors: { + onmouseover: [{ + action: 'set', + status: 'highlighted' + }], + onmouseout: [{ + action: 'unset', + status: 'highlighted' + }], + onclick: [{ + action: 'toggle', + status: 'selected', + exclusive: true + }], + onshiftclick: [{ + action: 'toggle', + status: 'selected' + }] + }, + tooltip: LocusZoom.Layouts.get('tooltip', 'standard_intervals', { unnamespaced: true }) + }); + /** + * Dashboard Layouts: toolbar buttons etc + * @namespace Layouts.dashboard + */ + LocusZoom.Layouts.add('dashboard', 'standard_panel', { + components: [ + { + type: 'remove_panel', + position: 'right', + color: 'red', + group_position: 'end' }, { - x: 2855381003, - text: "22", - style: { - "fill": "rgb(0, 0, 66)", - "text-anchor": "center", - "font-size": "13px", - "font-weight": "bold" - }, - transform: "translate(0, 2)" + type: 'move_panel_up', + position: 'right', + group_position: 'middle' + }, + { + type: 'move_panel_down', + position: 'right', + group_position: 'start', + style: { 'margin-left': '0.75em' } } ] - } - }, - data_layers: [ - LocusZoom.Layouts.get("data_layer", "genome_legend", { unnamespaced: true }) - ] -}); - -LocusZoom.Layouts.add("panel", "intervals", { - id: "intervals", - width: 1000, - height: 50, - min_width: 500, - min_height: 50, - margin: { top: 25, right: 150, bottom: 5, left: 50 }, - dashboard: (function(){ - var l = LocusZoom.Layouts.get("dashboard", "standard_panel", { unnamespaced: true }); - l.components.push({ - type: "toggle_split_tracks", - data_layer_id: "intervals", - position: "right" }); - return l; - })(), - axes: {}, - interaction: { - drag_background_to_pan: true, - scroll_to_zoom: true, - x_linked: true - }, - legend: { - hidden: true, - orientation: "horizontal", - origin: { x: 50, y: 0 }, - pad_from_bottom: 5 - }, - data_layers: [ - LocusZoom.Layouts.get("data_layer", "intervals", { unnamespaced: true }) - ] -}); - - -/** - * Plot Layouts - * @namespace Layouts.plot + LocusZoom.Layouts.add('dashboard', 'standard_plot', { + components: [ + { + type: 'title', + title: 'LocusZoom', + subtitle: 'v' + LocusZoom.version + '', + position: 'left' + }, + { + type: 'dimensions', + position: 'right' + }, + { + type: 'region_scale', + position: 'right' + }, + { + type: 'download', + position: 'right' + } + ] + }); + var covariates_model_plot_dashboard = LocusZoom.Layouts.get('dashboard', 'standard_plot'); + covariates_model_plot_dashboard.components.push({ + type: 'covariates_model', + button_html: 'Model', + button_title: 'Show and edit covariates currently in model', + position: 'left' + }); + LocusZoom.Layouts.add('dashboard', 'covariates_model_plot', covariates_model_plot_dashboard); + var region_nav_plot_dashboard = LocusZoom.Layouts.get('dashboard', 'standard_plot'); + region_nav_plot_dashboard.components.push({ + type: 'shift_region', + step: 500000, + button_html: '>>', + position: 'right', + group_position: 'end' + }); + region_nav_plot_dashboard.components.push({ + type: 'shift_region', + step: 50000, + button_html: '>', + position: 'right', + group_position: 'middle' + }); + region_nav_plot_dashboard.components.push({ + type: 'zoom_region', + step: 0.2, + position: 'right', + group_position: 'middle' + }); + region_nav_plot_dashboard.components.push({ + type: 'zoom_region', + step: -0.2, + position: 'right', + group_position: 'middle' + }); + region_nav_plot_dashboard.components.push({ + type: 'shift_region', + step: -50000, + button_html: '<', + position: 'right', + group_position: 'middle' + }); + region_nav_plot_dashboard.components.push({ + type: 'shift_region', + step: -500000, + button_html: '<<', + position: 'right', + group_position: 'start' + }); + LocusZoom.Layouts.add('dashboard', 'region_nav_plot', region_nav_plot_dashboard); + /** + * Panel Layouts + * @namespace Layouts.panel */ - -LocusZoom.Layouts.add("plot", "standard_association", { - state: {}, - width: 800, - height: 450, - responsive_resize: true, - min_region_scale: 20000, - max_region_scale: 1000000, - dashboard: LocusZoom.Layouts.get("dashboard", "standard_plot", { unnamespaced: true }), - panels: [ - LocusZoom.Layouts.get("panel", "association", { unnamespaced: true, proportional_height: 0.5 }), - LocusZoom.Layouts.get("panel", "genes", { unnamespaced: true, proportional_height: 0.5 }) - ] -}); - -// Shortcut to "StandardLayout" for backward compatibility -LocusZoom.StandardLayout = LocusZoom.Layouts.get("plot", "standard_association"); - -LocusZoom.Layouts.add("plot", "standard_phewas", { - width: 800, - height: 600, - min_width: 800, - min_height: 600, - responsive_resize: true, - dashboard: LocusZoom.Layouts.get("dashboard", "standard_plot", { unnamespaced: true } ), - panels: [ - LocusZoom.Layouts.get("panel", "phewas", { unnamespaced: true, proportional_height: 0.45 }), - LocusZoom.Layouts.get("panel", "genome_legend", { unnamespaced: true, proportional_height: 0.1 }), - LocusZoom.Layouts.get("panel", "genes", { - unnamespaced: true, proportional_height: 0.45, - margin: { bottom: 40 }, + LocusZoom.Layouts.add('panel', 'association', { + id: 'association', + width: 800, + height: 225, + min_width: 400, + min_height: 200, + proportional_width: 1, + margin: { + top: 35, + right: 50, + bottom: 40, + left: 50 + }, + inner_border: 'rgb(210, 210, 210)', + dashboard: function () { + var l = LocusZoom.Layouts.get('dashboard', 'standard_panel', { unnamespaced: true }); + l.components.push({ + type: 'toggle_legend', + position: 'right' + }); + return l; + }(), axes: { x: { - label: "Chromosome {{chr}} (Mb)", + label: 'Chromosome {{chr}} (Mb)', label_offset: 32, - tick_format: "region", - extent: "state" + tick_format: 'region', + extent: 'state' + }, + y1: { + label: '-log10 p-value', + label_offset: 28 + }, + y2: { + label: 'Recombination Rate (cM/Mb)', + label_offset: 40 } - } - }) - ], - mouse_guide: false -}); - -LocusZoom.Layouts.add("plot", "interval_association", { - state: {}, - width: 800, - height: 550, - responsive_resize: true, - min_region_scale: 20000, - max_region_scale: 1000000, - dashboard: LocusZoom.Layouts.get("dashboard", "standard_plot", { unnamespaced: true }), - panels: [ - LocusZoom.Layouts.get("panel", "association", { unnamespaced: true, width: 800, proportional_height: (225/570) }), - LocusZoom.Layouts.get("panel", "intervals", { unnamespaced: true, proportional_height: (120/570) }), - LocusZoom.Layouts.get("panel", "genes", { unnamespaced: true, width: 800, proportional_height: (225/570) }) - ] -}); - -/* global LocusZoom */ -"use strict"; - -/** - * A data layer is an abstract class representing a data set and its graphical representation within a panel - * @public - * @class - * @param {Object} layout A JSON-serializable object describing the layout for this layer - * @param {LocusZoom.DataLayer|LocusZoom.Panel} parent Where this layout is used -*/ -LocusZoom.DataLayer = function(layout, parent) { - /** @member {Boolean} */ - this.initialized = false; - /** @member {Number} */ - this.layout_idx = null; - - /** @member {String} */ - this.id = null; - /** @member {LocusZoom.Panel} */ - this.parent = parent || null; - /** - * @member {{group: d3.selection, container: d3.selection, clipRect: d3.selection}} - */ - this.svg = {}; - - /** @member {LocusZoom.Plot} */ - this.parent_plot = null; - if (typeof parent != "undefined" && parent instanceof LocusZoom.Panel){ this.parent_plot = parent.parent; } - - /** @member {Object} */ - this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.DataLayer.DefaultLayout); - if (this.layout.id){ this.id = this.layout.id; } - - // Ensure any axes defined in the layout have an explicit axis number (default: 1) - if (this.layout.x_axis !== {} && typeof this.layout.x_axis.axis !== "number"){ this.layout.x_axis.axis = 1; } - if (this.layout.y_axis !== {} && typeof this.layout.y_axis.axis !== "number"){ this.layout.y_axis.axis = 1; } - - /** @member {Object} */ - this.state = {}; - /** @member {String} */ - this.state_id = null; - - this.setDefaultState(); - - // Initialize parameters for storing data and tool tips - /** @member {Array} */ - this.data = []; - if (this.layout.tooltip){ - /** @member {Object} */ - this.tooltips = {}; - } - - // Initialize flags for tracking global statuses - this.global_statuses = { - "highlighted": false, - "selected": false, - "faded": false, - "hidden": false - }; - - return this; - -}; - -/** - * Instruct this datalayer to begin tracking additional fields from data sources (does not guarantee that such a field actually exists) - * - * Custom plots can use this to dynamically extend datalayer functionality after the plot is drawn - * - * (since removing core fields may break layer functionality, there is presently no hook for the inverse behavior) + }, + legend: { + orientation: 'vertical', + origin: { + x: 55, + y: 40 + }, + hidden: true + }, + interaction: { + drag_background_to_pan: true, + drag_x_ticks_to_scale: true, + drag_y1_ticks_to_scale: true, + drag_y2_ticks_to_scale: true, + scroll_to_zoom: true, + x_linked: true + }, + data_layers: [ + LocusZoom.Layouts.get('data_layer', 'significance', { unnamespaced: true }), + LocusZoom.Layouts.get('data_layer', 'recomb_rate', { unnamespaced: true }), + LocusZoom.Layouts.get('data_layer', 'association_pvalues', { unnamespaced: true }) + ] + }); + LocusZoom.Layouts.add('panel', 'genes', { + id: 'genes', + width: 800, + height: 225, + min_width: 400, + min_height: 112.5, + proportional_width: 1, + margin: { + top: 20, + right: 50, + bottom: 20, + left: 50 + }, + axes: {}, + interaction: { + drag_background_to_pan: true, + scroll_to_zoom: true, + x_linked: true + }, + dashboard: function () { + var l = LocusZoom.Layouts.get('dashboard', 'standard_panel', { unnamespaced: true }); + l.components.push({ + type: 'resize_to_data', + position: 'right' + }); + return l; + }(), + data_layers: [LocusZoom.Layouts.get('data_layer', 'genes', { unnamespaced: true })] + }); + LocusZoom.Layouts.add('panel', 'phewas', { + id: 'phewas', + width: 800, + height: 300, + min_width: 800, + min_height: 300, + proportional_width: 1, + margin: { + top: 20, + right: 50, + bottom: 120, + left: 50 + }, + inner_border: 'rgb(210, 210, 210)', + axes: { + x: { + ticks: { + // Object based config (shared defaults; allow layers to specify ticks) + style: { + 'font-weight': 'bold', + 'font-size': '11px', + 'text-anchor': 'start' + }, + transform: 'rotate(50)', + position: 'left' // Special param recognized by `category_scatter` layers + } + }, + y1: { + label: '-log10 p-value', + label_offset: 28 + } + }, + data_layers: [ + LocusZoom.Layouts.get('data_layer', 'significance', { unnamespaced: true }), + LocusZoom.Layouts.get('data_layer', 'phewas_pvalues', { unnamespaced: true }) + ] + }); + LocusZoom.Layouts.add('panel', 'genome_legend', { + id: 'genome_legend', + width: 800, + height: 50, + origin: { + x: 0, + y: 300 + }, + min_width: 800, + min_height: 50, + proportional_width: 1, + margin: { + top: 0, + right: 50, + bottom: 35, + left: 50 + }, + axes: { + x: { + label: 'Genomic Position (number denotes chromosome)', + label_offset: 35, + ticks: [ + { + x: 124625310, + text: '1', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 370850307, + text: '2', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 591461209, + text: '3', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 786049562, + text: '4', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 972084330, + text: '5', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1148099493, + text: '6', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1313226358, + text: '7', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1465977701, + text: '8', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1609766427, + text: '9', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1748140516, + text: '10', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 1883411148, + text: '11', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2017840353, + text: '12', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2142351240, + text: '13', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2253610949, + text: '14', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2358551415, + text: '15', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2454994487, + text: '16', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2540769469, + text: '17', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2620405698, + text: '18', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2689008813, + text: '19', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2750086065, + text: '20', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2805663772, + text: '21', + style: { + 'fill': 'rgb(120, 120, 186)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + }, + { + x: 2855381003, + text: '22', + style: { + 'fill': 'rgb(0, 0, 66)', + 'text-anchor': 'center', + 'font-size': '13px', + 'font-weight': 'bold' + }, + transform: 'translate(0, 2)' + } + ] + } + }, + data_layers: [LocusZoom.Layouts.get('data_layer', 'genome_legend', { unnamespaced: true })] + }); + LocusZoom.Layouts.add('panel', 'intervals', { + id: 'intervals', + width: 1000, + height: 50, + min_width: 500, + min_height: 50, + margin: { + top: 25, + right: 150, + bottom: 5, + left: 50 + }, + dashboard: function () { + var l = LocusZoom.Layouts.get('dashboard', 'standard_panel', { unnamespaced: true }); + l.components.push({ + type: 'toggle_split_tracks', + data_layer_id: 'intervals', + position: 'right' + }); + return l; + }(), + axes: {}, + interaction: { + drag_background_to_pan: true, + scroll_to_zoom: true, + x_linked: true + }, + legend: { + hidden: true, + orientation: 'horizontal', + origin: { + x: 50, + y: 0 + }, + pad_from_bottom: 5 + }, + data_layers: [LocusZoom.Layouts.get('data_layer', 'intervals', { unnamespaced: true })] + }); + /** + * Plot Layouts + * @namespace Layouts.plot + */ + LocusZoom.Layouts.add('plot', 'standard_association', { + state: {}, + width: 800, + height: 450, + responsive_resize: true, + min_region_scale: 20000, + max_region_scale: 1000000, + dashboard: LocusZoom.Layouts.get('dashboard', 'standard_plot', { unnamespaced: true }), + panels: [ + LocusZoom.Layouts.get('panel', 'association', { + unnamespaced: true, + proportional_height: 0.5 + }), + LocusZoom.Layouts.get('panel', 'genes', { + unnamespaced: true, + proportional_height: 0.5 + }) + ] + }); + // Shortcut to "StandardLayout" for backward compatibility + LocusZoom.StandardLayout = LocusZoom.Layouts.get('plot', 'standard_association'); + LocusZoom.Layouts.add('plot', 'standard_phewas', { + width: 800, + height: 600, + min_width: 800, + min_height: 600, + responsive_resize: true, + dashboard: LocusZoom.Layouts.get('dashboard', 'standard_plot', { unnamespaced: true }), + panels: [ + LocusZoom.Layouts.get('panel', 'phewas', { + unnamespaced: true, + proportional_height: 0.45 + }), + LocusZoom.Layouts.get('panel', 'genome_legend', { + unnamespaced: true, + proportional_height: 0.1 + }), + LocusZoom.Layouts.get('panel', 'genes', { + unnamespaced: true, + proportional_height: 0.45, + margin: { bottom: 40 }, + axes: { + x: { + label: 'Chromosome {{chr}} (Mb)', + label_offset: 32, + tick_format: 'region', + extent: 'state' + } + } + }) + ], + mouse_guide: false + }); + LocusZoom.Layouts.add('plot', 'interval_association', { + state: {}, + width: 800, + height: 550, + responsive_resize: true, + min_region_scale: 20000, + max_region_scale: 1000000, + dashboard: LocusZoom.Layouts.get('dashboard', 'standard_plot', { unnamespaced: true }), + panels: [ + LocusZoom.Layouts.get('panel', 'association', { + unnamespaced: true, + width: 800, + proportional_height: 225 / 570 + }), + LocusZoom.Layouts.get('panel', 'intervals', { + unnamespaced: true, + proportional_height: 120 / 570 + }), + LocusZoom.Layouts.get('panel', 'genes', { + unnamespaced: true, + width: 800, + proportional_height: 225 / 570 + }) + ] + }); + /* global LocusZoom */ + 'use strict'; + /** + * A data layer is an abstract class representing a data set and its graphical representation within a panel + * @public + * @class + * @param {Object} layout A JSON-serializable object describing the layout for this layer + * @param {LocusZoom.DataLayer|LocusZoom.Panel} parent Where this layout is used +*/ + LocusZoom.DataLayer = function (layout, parent) { + /** @member {Boolean} */ + this.initialized = false; + /** @member {Number} */ + this.layout_idx = null; + /** @member {String} */ + this.id = null; + /** @member {LocusZoom.Panel} */ + this.parent = parent || null; + /** + * @member {{group: d3.selection, container: d3.selection, clipRect: d3.selection}} + */ + this.svg = {}; + /** @member {LocusZoom.Plot} */ + this.parent_plot = null; + if (typeof parent != 'undefined' && parent instanceof LocusZoom.Panel) { + this.parent_plot = parent.parent; + } + /** @member {Object} */ + this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.DataLayer.DefaultLayout); + if (this.layout.id) { + this.id = this.layout.id; + } + // Ensure any axes defined in the layout have an explicit axis number (default: 1) + if (this.layout.x_axis !== {} && typeof this.layout.x_axis.axis !== 'number') { + this.layout.x_axis.axis = 1; + } + if (this.layout.y_axis !== {} && typeof this.layout.y_axis.axis !== 'number') { + this.layout.y_axis.axis = 1; + } + /** + * Values in the layout object may change during rendering etc. Retain a copy of the original data layer state + * @member {Object} + */ + this._base_layout = JSON.parse(JSON.stringify(this.layout)); + /** @member {Object} */ + this.state = {}; + /** @member {String} */ + this.state_id = null; + this.setDefaultState(); + // Initialize parameters for storing data and tool tips + /** @member {Array} */ + this.data = []; + if (this.layout.tooltip) { + /** @member {Object} */ + this.tooltips = {}; + } + // Initialize flags for tracking global statuses + this.global_statuses = { + 'highlighted': false, + 'selected': false, + 'faded': false, + 'hidden': false + }; + return this; + }; + /** + * Instruct this datalayer to begin tracking additional fields from data sources (does not guarantee that such a field actually exists) + * + * Custom plots can use this to dynamically extend datalayer functionality after the plot is drawn + * + * (since removing core fields may break layer functionality, there is presently no hook for the inverse behavior) * @param fieldName * @param namespace * @param {String|String[]} transformations The name (or array of names) of transformations to apply to this field * @returns {String} The raw string added to the fields array */ -LocusZoom.DataLayer.prototype.addField = function(fieldName, namespace, transformations) { - if (!fieldName || !namespace) { - throw "Must specify field name and namespace to use when adding field"; - } - var fieldString = namespace + ":" + fieldName; - if (transformations) { - fieldString += "|"; - if (typeof transformations === "string") { - fieldString += transformations; - } else if (Array.isArray(transformations)) { - fieldString += transformations.join("|"); - } else { - throw "Must provide transformations as either a string or array of strings"; - } - } - var fields = this.layout.fields; - if (fields.indexOf(fieldString) === -1) { - fields.push(fieldString); - } - return fieldString; -}; - -/** + LocusZoom.DataLayer.prototype.addField = function (fieldName, namespace, transformations) { + if (!fieldName || !namespace) { + throw 'Must specify field name and namespace to use when adding field'; + } + var fieldString = namespace + ':' + fieldName; + if (transformations) { + fieldString += '|'; + if (typeof transformations === 'string') { + fieldString += transformations; + } else if (Array.isArray(transformations)) { + fieldString += transformations.join('|'); + } else { + throw 'Must provide transformations as either a string or array of strings'; + } + } + var fields = this.layout.fields; + if (fields.indexOf(fieldString) === -1) { + fields.push(fieldString); + } + return fieldString; + }; + /** * Define default state that should get tracked during the lifetime of this layer. * * In some special custom usages, it may be useful to completely reset a panel (eg "click for * genome region" links), plotting new data that invalidates any previously tracked state. This hook makes it * possible to reset without destroying the panel entirely. It is used by `Plot.clearPanelData`. */ -LocusZoom.DataLayer.prototype.setDefaultState = function() { - // Define state parameters specific to this data layer - if (this.parent){ - this.state = this.parent.state; - this.state_id = this.parent.id + "." + this.id; - this.state[this.state_id] = this.state[this.state_id] || {}; - LocusZoom.DataLayer.Statuses.adjectives.forEach(function(status){ - this.state[this.state_id][status] = this.state[this.state_id][status] || []; - }.bind(this)); - } -}; - -/** + LocusZoom.DataLayer.prototype.setDefaultState = function () { + // Define state parameters specific to this data layer + if (this.parent) { + this.state = this.parent.state; + this.state_id = this.parent.id + '.' + this.id; + this.state[this.state_id] = this.state[this.state_id] || {}; + LocusZoom.DataLayer.Statuses.adjectives.forEach(function (status) { + this.state[this.state_id][status] = this.state[this.state_id][status] || []; + }.bind(this)); + } + }; + /** * A basic description of keys expected in a layout. Not intended to be directly used or modified by an end user. * @protected * @type {{type: string, fields: Array, x_axis: {}, y_axis: {}}} */ -LocusZoom.DataLayer.DefaultLayout = { - type: "", - fields: [], - x_axis: {}, - y_axis: {} -}; - -/** + LocusZoom.DataLayer.DefaultLayout = { + type: '', + fields: [], + x_axis: {}, + y_axis: {} + }; + /** * Available statuses that individual elements can have. Each status is described by * a verb/antiverb and an adjective. Verbs and antiverbs are used to generate data layer * methods for updating the status on one or more elements. Adjectives are used in class @@ -2005,22 +2273,35 @@ LocusZoom.DataLayer.DefaultLayout = { * @static * @type {{verbs: String[], adjectives: String[], menu_antiverbs: String[]}} */ -LocusZoom.DataLayer.Statuses = { - verbs: ["highlight", "select", "fade", "hide"], - adjectives: ["highlighted", "selected", "faded", "hidden"], - menu_antiverbs: ["unhighlight", "deselect", "unfade", "show"] -}; - -/** + LocusZoom.DataLayer.Statuses = { + verbs: [ + 'highlight', + 'select', + 'fade', + 'hide' + ], + adjectives: [ + 'highlighted', + 'selected', + 'faded', + 'hidden' + ], + menu_antiverbs: [ + 'unhighlight', + 'deselect', + 'unfade', + 'show' + ] + }; + /** * Get the fully qualified identifier for the data layer, prefixed by any parent or container elements * * @returns {string} A dot-delimited string of the format .. */ -LocusZoom.DataLayer.prototype.getBaseId = function(){ - return this.parent_plot.id + "." + this.parent.id + "." + this.id; -}; - -/** + LocusZoom.DataLayer.prototype.getBaseId = function () { + return this.parent_plot.id + '.' + this.parent.id + '.' + this.id; + }; + /** * Determine the pixel height of data-bound objects represented inside this data layer. (excluding elements such as axes) * * May be used by operations that resize the data layer to fit available data @@ -2028,41 +2309,40 @@ LocusZoom.DataLayer.prototype.getBaseId = function(){ * @public * @returns {number} */ -LocusZoom.DataLayer.prototype.getAbsoluteDataHeight = function(){ - var dataBCR = this.svg.group.node().getBoundingClientRect(); - return dataBCR.height; -}; - -/** + LocusZoom.DataLayer.prototype.getAbsoluteDataHeight = function () { + var dataBCR = this.svg.group.node().getBoundingClientRect(); + return dataBCR.height; + }; + /** * Whether transitions can be applied to this data layer * @returns {boolean} */ -LocusZoom.DataLayer.prototype.canTransition = function(){ - if (!this.layout.transition){ return false; } - return !(this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.panel_id); -}; - -/** + LocusZoom.DataLayer.prototype.canTransition = function () { + if (!this.layout.transition) { + return false; + } + return !(this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.panel_id); + }; + /** * Fetch the fully qualified ID to be associated with a specific visual element, based on the data to which that * element is bound. In general this element ID will be unique, allowing it to be addressed directly via selectors. * @param {String|Object} element * @returns {String} */ -LocusZoom.DataLayer.prototype.getElementId = function(element){ - var element_id = "element"; - if (typeof element == "string"){ - element_id = element; - } else if (typeof element == "object"){ - var id_field = this.layout.id_field || "id"; - if (typeof element[id_field] == "undefined"){ - throw("Unable to generate element ID"); - } - element_id = element[id_field].toString().replace(/\W/g,""); - } - return (this.getBaseId() + "-" + element_id).replace(/(:|\.|\[|\]|,)/g, "_"); -}; - -/** + LocusZoom.DataLayer.prototype.getElementId = function (element) { + var element_id = 'element'; + if (typeof element == 'string') { + element_id = element; + } else if (typeof element == 'object') { + var id_field = this.layout.id_field || 'id'; + if (typeof element[id_field] == 'undefined') { + throw 'Unable to generate element ID'; + } + element_id = element[id_field].toString().replace(/\W/g, ''); + } + return (this.getBaseId() + '-' + element_id).replace(/(:|\.|\[|\]|,)/g, '_'); + }; + /** * Fetch an ID that may bind a data element to a separate visual node for displaying status * Examples of this might be seperate visual nodes to show select/highlight statuses, or * even a common/shared node to show status across many elements in a set. @@ -2071,219 +2351,203 @@ LocusZoom.DataLayer.prototype.getElementId = function(element){ * @param {String|Object} element * @returns {String|null} */ -LocusZoom.DataLayer.prototype.getElementStatusNodeId = function(element){ - return null; -}; - -/** + LocusZoom.DataLayer.prototype.getElementStatusNodeId = function (element) { + return null; + }; + /** * Returns a reference to the underlying data associated with a single visual element in the data layer, as * referenced by the unique identifier for the element * @param {String} id The unique identifier for the element, as defined by `getElementId` * @returns {Object|null} The data bound to that element */ -LocusZoom.DataLayer.prototype.getElementById = function(id){ - var selector = d3.select("#" + id.replace(/(:|\.|\[|\]|,)/g, "\\$1")); - if (!selector.empty() && selector.data() && selector.data().length){ - return selector.data()[0]; - } else { - return null; - } -}; - -/** + LocusZoom.DataLayer.prototype.getElementById = function (id) { + var selector = d3.select('#' + id.replace(/(:|\.|\[|\]|,)/g, '\\$1')); + if (!selector.empty() && selector.data() && selector.data().length) { + return selector.data()[0]; + } else { + return null; + } + }; + /** * Basic method to apply arbitrary methods and properties to data elements. * This is called on all data immediately after being fetched. * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.applyDataMethods = function(){ - this.data.forEach(function(d, i){ - // Basic toHTML() method - return the stringified value in the id_field, if defined. - this.data[i].toHTML = function(){ - var id_field = this.layout.id_field || "id"; - var html = ""; - if (this.data[i][id_field]){ html = this.data[i][id_field].toString(); } - return html; - }.bind(this); - // getDataLayer() method - return a reference to the data layer - this.data[i].getDataLayer = function(){ + LocusZoom.DataLayer.prototype.applyDataMethods = function () { + this.data.forEach(function (d, i) { + // Basic toHTML() method - return the stringified value in the id_field, if defined. + this.data[i].toHTML = function () { + var id_field = this.layout.id_field || 'id'; + var html = ''; + if (this.data[i][id_field]) { + html = this.data[i][id_field].toString(); + } + return html; + }.bind(this); + // getDataLayer() method - return a reference to the data layer + this.data[i].getDataLayer = function () { + return this; + }.bind(this); + // deselect() method - shortcut method to deselect the element + this.data[i].deselect = function () { + var data_layer = this.getDataLayer(); + data_layer.unselectElement(this); + }; + }.bind(this)); + this.applyCustomDataMethods(); return this; - }.bind(this); - // deselect() method - shortcut method to deselect the element - this.data[i].deselect = function(){ - var data_layer = this.getDataLayer(); - data_layer.unselectElement(this); - }; - }.bind(this)); - this.applyCustomDataMethods(); - return this; -}; - -/** + }; + /** * Hook that allows custom datalayers to apply additional methods and properties to data elements as needed * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.applyCustomDataMethods = function(){ - return this; -}; - -/** + LocusZoom.DataLayer.prototype.applyCustomDataMethods = function () { + return this; + }; + /** * Initialize a data layer * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.initialize = function(){ - - // Append a container group element to house the main data layer group element and the clip path - this.svg.container = this.parent.svg.group.append("g") - .attr("class", "lz-data_layer-container") - .attr("id", this.getBaseId() + ".data_layer_container"); - - // Append clip path to the container element - this.svg.clipRect = this.svg.container.append("clipPath") - .attr("id", this.getBaseId() + ".clip") - .append("rect"); - - // Append svg group for rendering all data layer elements, clipped by the clip path - this.svg.group = this.svg.container.append("g") - .attr("id", this.getBaseId() + ".data_layer") - .attr("clip-path", "url(#" + this.getBaseId() + ".clip)"); - - return this; - -}; - -/** + LocusZoom.DataLayer.prototype.initialize = function () { + // Append a container group element to house the main data layer group element and the clip path + this.svg.container = this.parent.svg.group.append('g').attr('class', 'lz-data_layer-container').attr('id', this.getBaseId() + '.data_layer_container'); + // Append clip path to the container element + this.svg.clipRect = this.svg.container.append('clipPath').attr('id', this.getBaseId() + '.clip').append('rect'); + // Append svg group for rendering all data layer elements, clipped by the clip path + this.svg.group = this.svg.container.append('g').attr('id', this.getBaseId() + '.data_layer').attr('clip-path', 'url(#' + this.getBaseId() + '.clip)'); + return this; + }; + /** * Move a data layer up relative to others by z-index * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.moveUp = function(){ - if (this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1]){ - this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1]; - this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1] = this.id; - this.parent.resortDataLayers(); - } - return this; -}; - -/** + LocusZoom.DataLayer.prototype.moveUp = function () { + if (this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1]) { + this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1]; + this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1] = this.id; + this.parent.resortDataLayers(); + } + return this; + }; + /** * Move a data layer down relative to others by z-index * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.moveDown = function(){ - if (this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1]){ - this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1]; - this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1] = this.id; - this.parent.resortDataLayers(); - } - return this; -}; - -/** + LocusZoom.DataLayer.prototype.moveDown = function () { + if (this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1]) { + this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1]; + this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1] = this.id; + this.parent.resortDataLayers(); + } + return this; + }; + /** * Apply scaling functions to an element or parameter as needed, based on its layout and the element's data * If the layout parameter is already a primitive type, simply return the value as given * @param {Array|Number|String|Object} layout * @param {*} data The value to be used with the filter * @returns {*} The transformed value */ -LocusZoom.DataLayer.prototype.resolveScalableParameter = function(layout, data){ - var ret = null; - if (Array.isArray(layout)){ - var idx = 0; - while (ret === null && idx < layout.length){ - ret = this.resolveScalableParameter(layout[idx], data); - idx++; - } - } else { - switch (typeof layout){ - case "number": - case "string": - ret = layout; - break; - case "object": - if (layout.scale_function){ - if(layout.field) { - var f = new LocusZoom.Data.Field(layout.field); - ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, f.resolve(data)); - } else { - ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, data); + LocusZoom.DataLayer.prototype.resolveScalableParameter = function (layout, data) { + var ret = null; + if (Array.isArray(layout)) { + var idx = 0; + while (ret === null && idx < layout.length) { + ret = this.resolveScalableParameter(layout[idx], data); + idx++; + } + } else { + switch (typeof layout) { + case 'number': + case 'string': + ret = layout; + break; + case 'object': + if (layout.scale_function) { + if (layout.field) { + var f = new LocusZoom.Data.Field(layout.field); + ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, f.resolve(data)); + } else { + ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, data); + } + } + break; } } - break; - } - } - return ret; -}; - -/** + return ret; + }; + /** * Generate dimension extent function based on layout parameters * @param {('x'|'y')} dimension */ -LocusZoom.DataLayer.prototype.getAxisExtent = function(dimension){ - - if (["x", "y"].indexOf(dimension) === -1){ - throw("Invalid dimension identifier passed to LocusZoom.DataLayer.getAxisExtent()"); - } - - var axis = dimension + "_axis"; - - // If a floor AND a ceiling are explicitly defined then just return that extent and be done - if (!isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].ceiling)){ - return [+this.layout[axis].floor, +this.layout[axis].ceiling]; - } - - // If a field is defined for the axis and the data layer has data then generate the extent from the data set - if (this.layout[axis].field && this.data && this.data.length){ - - var extent = d3.extent(this.data, function(d) { - var f = new LocusZoom.Data.Field(this.layout[axis].field); - return +f.resolve(d); - }.bind(this)); - - // Apply floor/ceiling - if (!isNaN(this.layout[axis].floor)) { - extent[0] = this.layout[axis].floor; - extent[1] = d3.max(extent); - } - if (!isNaN(this.layout[axis].ceiling)) { - extent[1] = this.layout[axis].ceiling; - extent[0] = d3.min(extent); - } - - // Apply upper/lower buffers, if applicable - var original_extent_span = extent[1] - extent[0]; - if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].lower_buffer)) { - extent[0] -= original_extent_span * this.layout[axis].lower_buffer; - } - if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].upper_buffer)) { - extent[1] += original_extent_span * this.layout[axis].upper_buffer; - } - - // Apply minimum extent - if (typeof this.layout[axis].min_extent == "object") { - if (isNaN(this.layout[axis].floor) && !isNaN(this.layout[axis].min_extent[0])) { - extent[0] = Math.min(extent[0], this.layout[axis].min_extent[0]); + LocusZoom.DataLayer.prototype.getAxisExtent = function (dimension) { + if ([ + 'x', + 'y' + ].indexOf(dimension) === -1) { + throw 'Invalid dimension identifier passed to LocusZoom.DataLayer.getAxisExtent()'; } - if (isNaN(this.layout[axis].ceiling) && !isNaN(this.layout[axis].min_extent[1])) { - extent[1] = Math.max(extent[1], this.layout[axis].min_extent[1]); + var axis_name = dimension + '_axis'; + var axis_layout = this.layout[axis_name]; + // If a floor AND a ceiling are explicitly defined then just return that extent and be done + if (!isNaN(axis_layout.floor) && !isNaN(axis_layout.ceiling)) { + return [ + +axis_layout.floor, + +axis_layout.ceiling + ]; } - } - - return extent; - - } - - // If this is for the x axis and no extent could be generated yet but state has a defined start and end - // then default to using the state-defined region as the extent - if (dimension === "x" && !isNaN(this.state.start) && !isNaN(this.state.end)) { - return [this.state.start, this.state.end]; - } - - // No conditions met for generating a valid extent, return an empty array - return []; - -}; - -/** + // If a field is defined for the axis and the data layer has data then generate the extent from the data set + var data_extent = []; + if (axis_layout.field && this.data) { + if (!this.data.length) { + // If data has been fetched (but no points in region), enforce the min_extent (with no buffers, + // because we don't need padding around an empty screen) + data_extent = axis_layout.min_extent || []; + return data_extent; + } else { + data_extent = d3.extent(this.data, function (d) { + var f = new LocusZoom.Data.Field(axis_layout.field); + return +f.resolve(d); + }); + // Apply upper/lower buffers, if applicable + var original_extent_span = data_extent[1] - data_extent[0]; + if (!isNaN(axis_layout.lower_buffer)) { + data_extent[0] -= original_extent_span * axis_layout.lower_buffer; + } + if (!isNaN(axis_layout.upper_buffer)) { + data_extent[1] += original_extent_span * axis_layout.upper_buffer; + } + if (typeof axis_layout.min_extent == 'object') { + // The data should span at least the range specified by min_extent, an array with [low, high] + var range_min = axis_layout.min_extent[0]; + var range_max = axis_layout.min_extent[1]; + if (!isNaN(range_min) && !isNaN(range_max)) { + data_extent[0] = Math.min(data_extent[0], range_min); + } + if (!isNaN(range_max)) { + data_extent[1] = Math.max(data_extent[1], range_max); + } + } + // If specified, floor and ceiling will override the actual data range + return [ + isNaN(axis_layout.floor) ? data_extent[0] : axis_layout.floor, + isNaN(axis_layout.ceiling) ? data_extent[1] : axis_layout.ceiling + ]; + } + } + // If this is for the x axis and no extent could be generated yet but state has a defined start and end + // then default to using the state-defined region as the extent + if (dimension === 'x' && !isNaN(this.state.start) && !isNaN(this.state.end)) { + return [ + this.state.start, + this.state.end + ]; + } + // No conditions met for generating a valid extent, return an empty array + return []; + }; + /** * Allow this data layer to tell the panel what axis ticks it thinks it will require. The panel may choose whether * to use some, all, or none of these when rendering, either alone or in conjunction with other data layers. * @@ -2300,220 +2564,204 @@ LocusZoom.DataLayer.prototype.getAxisExtent = function(dimension){ * * transform: SVG transform attribute string * * color: string or LocusZoom scalable parameter object */ -LocusZoom.DataLayer.prototype.getTicks = function (dimension, config) { - if (["x", "y"].indexOf(dimension) === -1) { - throw("Invalid dimension identifier"); - } - return []; -}; - -/** + LocusZoom.DataLayer.prototype.getTicks = function (dimension, config) { + if ([ + 'x', + 'y' + ].indexOf(dimension) === -1) { + throw 'Invalid dimension identifier'; + } + return []; + }; + /** * Generate a tool tip for a given element * @param {String|Object} d The element associated with the tooltip * @param {String} [id] An identifier to the tooltip */ -LocusZoom.DataLayer.prototype.createTooltip = function(d, id){ - if (typeof this.layout.tooltip != "object"){ - throw ("DataLayer [" + this.id + "] layout does not define a tooltip"); - } - if (typeof id == "undefined"){ id = this.getElementId(d); } - if (this.tooltips[id]){ - this.positionTooltip(id); - return; - } - this.tooltips[id] = { - data: d, - arrow: null, - selector: d3.select(this.parent_plot.svg.node().parentNode).append("div") - .attr("class", "lz-data_layer-tooltip") - .attr("id", id + "-tooltip") - }; - this.updateTooltip(d); - return this; -}; - -/** + LocusZoom.DataLayer.prototype.createTooltip = function (d, id) { + if (typeof this.layout.tooltip != 'object') { + throw 'DataLayer [' + this.id + '] layout does not define a tooltip'; + } + if (typeof id == 'undefined') { + id = this.getElementId(d); + } + if (this.tooltips[id]) { + this.positionTooltip(id); + return; + } + this.tooltips[id] = { + data: d, + arrow: null, + selector: d3.select(this.parent_plot.svg.node().parentNode).append('div').attr('class', 'lz-data_layer-tooltip').attr('id', id + '-tooltip') + }; + this.updateTooltip(d); + return this; + }; + /** * Update a tool tip (generate its inner HTML) * @param {String|Object} d The element associated with the tooltip * @param {String} [id] An identifier to the tooltip */ -LocusZoom.DataLayer.prototype.updateTooltip = function(d, id){ - if (typeof id == "undefined"){ id = this.getElementId(d); } - // Empty the tooltip of all HTML (including its arrow!) - this.tooltips[id].selector.html(""); - this.tooltips[id].arrow = null; - // Set the new HTML - if (this.layout.tooltip.html){ - this.tooltips[id].selector.html(LocusZoom.parseFields(d, this.layout.tooltip.html)); - } - // If the layout allows tool tips on this data layer to be closable then add the close button - // and add padding to the tooltip to accommodate it - if (this.layout.tooltip.closable){ - this.tooltips[id].selector.insert("button", ":first-child") - .attr("class", "lz-tooltip-close-button") - .attr("title", "Close") - .text("×") - .on("click", function(){ - this.destroyTooltip(id); - }.bind(this)); - } - // Apply data directly to the tool tip for easier retrieval by custom UI elements inside the tool tip - this.tooltips[id].selector.data([d]); - // Reposition and draw a new arrow - this.positionTooltip(id); - return this; -}; - -/** - * Destroy tool tip - remove the tool tip element from the DOM and delete the tool tip's record on the data layer - * @param {String|Object} d The element associated with the tooltip - * @param {String} [id] An identifier to the tooltip + LocusZoom.DataLayer.prototype.updateTooltip = function (d, id) { + if (typeof id == 'undefined') { + id = this.getElementId(d); + } + // Empty the tooltip of all HTML (including its arrow!) + this.tooltips[id].selector.html(''); + this.tooltips[id].arrow = null; + // Set the new HTML + if (this.layout.tooltip.html) { + this.tooltips[id].selector.html(LocusZoom.parseFields(d, this.layout.tooltip.html)); + } + // If the layout allows tool tips on this data layer to be closable then add the close button + // and add padding to the tooltip to accommodate it + if (this.layout.tooltip.closable) { + this.tooltips[id].selector.insert('button', ':first-child').attr('class', 'lz-tooltip-close-button').attr('title', 'Close').text('\xD7').on('click', function () { + this.destroyTooltip(id); + }.bind(this)); + } + // Apply data directly to the tool tip for easier retrieval by custom UI elements inside the tool tip + this.tooltips[id].selector.data([d]); + // Reposition and draw a new arrow + this.positionTooltip(id); + return this; + }; + /** + * Destroy tool tip - remove the tool tip element from the DOM and delete the tool tip's record on the data layer + * @param {String|Object} d The element associated with the tooltip + * @param {String} [id] An identifier to the tooltip * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.destroyTooltip = function(d, id){ - if (typeof d == "string"){ - id = d; - } else if (typeof id == "undefined"){ - id = this.getElementId(d); - } - if (this.tooltips[id]){ - if (typeof this.tooltips[id].selector == "object"){ - this.tooltips[id].selector.remove(); - } - delete this.tooltips[id]; - } - return this; -}; - -/** + LocusZoom.DataLayer.prototype.destroyTooltip = function (d, id) { + if (typeof d == 'string') { + id = d; + } else if (typeof id == 'undefined') { + id = this.getElementId(d); + } + if (this.tooltips[id]) { + if (typeof this.tooltips[id].selector == 'object') { + this.tooltips[id].selector.remove(); + } + delete this.tooltips[id]; + } + return this; + }; + /** * Loop through and destroy all tool tips on this data layer * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.destroyAllTooltips = function(){ - for (var id in this.tooltips){ - this.destroyTooltip(id); - } - return this; -}; - -// -/** + LocusZoom.DataLayer.prototype.destroyAllTooltips = function () { + for (var id in this.tooltips) { + this.destroyTooltip(id); + } + return this; + }; + // + /** * Position tool tip - naïve function to place a tool tip to the lower right of the current mouse element * Most data layers reimplement this method to position tool tips specifically for the data they display * @param {String} id The identifier of the tooltip to position * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - // Position the div itself - this.tooltips[id].selector - .style("left", (d3.event.pageX) + "px") - .style("top", (d3.event.pageY) + "px"); - // Create / update position on arrow connecting tooltip to data - if (!this.tooltips[id].arrow){ - this.tooltips[id].arrow = this.tooltips[id].selector.append("div") - .style("position", "absolute") - .attr("class", "lz-data_layer-tooltip-arrow_top_left"); - } - this.tooltips[id].arrow - .style("left", "-1px") - .style("top", "-1px"); - return this; -}; - -/** + LocusZoom.DataLayer.prototype.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + // Position the div itself + this.tooltips[id].selector.style('left', d3.event.pageX + 'px').style('top', d3.event.pageY + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!this.tooltips[id].arrow) { + this.tooltips[id].arrow = this.tooltips[id].selector.append('div').style('position', 'absolute').attr('class', 'lz-data_layer-tooltip-arrow_top_left'); + } + this.tooltips[id].arrow.style('left', '-1px').style('top', '-1px'); + return this; + }; + /** * Loop through and position all tool tips on this data layer * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.positionAllTooltips = function(){ - for (var id in this.tooltips){ - this.positionTooltip(id); - } - return this; -}; - -/** + LocusZoom.DataLayer.prototype.positionAllTooltips = function () { + for (var id in this.tooltips) { + this.positionTooltip(id); + } + return this; + }; + /** * Show or hide a tool tip by ID depending on directives in the layout and state values relative to the ID * @param {String|Object} element The element associated with the tooltip * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.showOrHideTooltip = function(element){ - - if (typeof this.layout.tooltip != "object"){ return; } - var id = this.getElementId(element); - - var resolveStatus = function(statuses, directive, operator){ - var status = null; - if (typeof statuses != "object" || statuses === null){ return null; } - if (Array.isArray(directive)){ - if (typeof operator == "undefined"){ operator = "and"; } - if (directive.length === 1){ - status = statuses[directive[0]]; - } else { - status = directive.reduce(function(previousValue, currentValue) { - if (operator === "and"){ - return statuses[previousValue] && statuses[currentValue]; - } else if (operator === "or"){ - return statuses[previousValue] || statuses[currentValue]; - } - return null; - }); + LocusZoom.DataLayer.prototype.showOrHideTooltip = function (element) { + if (typeof this.layout.tooltip != 'object') { + return; } - } else if (typeof directive == "object"){ - var sub_status; - for (var sub_operator in directive){ - sub_status = resolveStatus(statuses, directive[sub_operator], sub_operator); - if (status === null){ - status = sub_status; - } else if (operator === "and"){ - status = status && sub_status; - } else if (operator === "or"){ - status = status || sub_status; + var id = this.getElementId(element); + var resolveStatus = function (statuses, directive, operator) { + var status = null; + if (typeof statuses != 'object' || statuses === null) { + return null; } + if (Array.isArray(directive)) { + if (typeof operator == 'undefined') { + operator = 'and'; + } + if (directive.length === 1) { + status = statuses[directive[0]]; + } else { + status = directive.reduce(function (previousValue, currentValue) { + if (operator === 'and') { + return statuses[previousValue] && statuses[currentValue]; + } else if (operator === 'or') { + return statuses[previousValue] || statuses[currentValue]; + } + return null; + }); + } + } else if (typeof directive == 'object') { + var sub_status; + for (var sub_operator in directive) { + sub_status = resolveStatus(statuses, directive[sub_operator], sub_operator); + if (status === null) { + status = sub_status; + } else if (operator === 'and') { + status = status && sub_status; + } else if (operator === 'or') { + status = status || sub_status; + } + } + } + return status; + }; + var show_directive = {}; + if (typeof this.layout.tooltip.show == 'string') { + show_directive = { and: [this.layout.tooltip.show] }; + } else if (typeof this.layout.tooltip.show == 'object') { + show_directive = this.layout.tooltip.show; } - } - return status; - }; - - var show_directive = {}; - if (typeof this.layout.tooltip.show == "string"){ - show_directive = { and: [ this.layout.tooltip.show ] }; - } else if (typeof this.layout.tooltip.show == "object"){ - show_directive = this.layout.tooltip.show; - } - - var hide_directive = {}; - if (typeof this.layout.tooltip.hide == "string"){ - hide_directive = { and: [ this.layout.tooltip.hide ] }; - } else if (typeof this.layout.tooltip.hide == "object"){ - hide_directive = this.layout.tooltip.hide; - } - - var statuses = {}; - LocusZoom.DataLayer.Statuses.adjectives.forEach(function(status){ - var antistatus = "un" + status; - statuses[status] = this.state[this.state_id][status].indexOf(id) !== -1; - statuses[antistatus] = !statuses[status]; - }.bind(this)); - - var show_resolved = resolveStatus(statuses, show_directive); - var hide_resolved = resolveStatus(statuses, hide_directive); - - // Only show tooltip if the resolved logic explicitly shows and explicitly not hides the tool tip - // Otherwise ensure tooltip does not exist - if (show_resolved && !hide_resolved){ - this.createTooltip(element); - } else { - this.destroyTooltip(element); - } - - return this; - -}; - -/** + var hide_directive = {}; + if (typeof this.layout.tooltip.hide == 'string') { + hide_directive = { and: [this.layout.tooltip.hide] }; + } else if (typeof this.layout.tooltip.hide == 'object') { + hide_directive = this.layout.tooltip.hide; + } + var statuses = {}; + LocusZoom.DataLayer.Statuses.adjectives.forEach(function (status) { + var antistatus = 'un' + status; + statuses[status] = this.state[this.state_id][status].indexOf(id) !== -1; + statuses[antistatus] = !statuses[status]; + }.bind(this)); + var show_resolved = resolveStatus(statuses, show_directive); + var hide_resolved = resolveStatus(statuses, hide_directive); + // Only show tooltip if the resolved logic explicitly shows and explicitly not hides the tool tip + // Otherwise ensure tooltip does not exist + if (show_resolved && !hide_resolved) { + this.createTooltip(element); + } else { + this.destroyTooltip(element); + } + return this; + }; + /** * Find the elements (or indices) that match any of a set of provided filters * @protected * @param {Array[]} filters A list of filter entries: [field, value] (for equivalence testing) or @@ -2522,87 +2770,127 @@ LocusZoom.DataLayer.prototype.showOrHideTooltip = function(element){ * elements, or references to the elements themselves * @returns {Array} */ -LocusZoom.DataLayer.prototype.filter = function(filters, return_type){ - if (typeof return_type == "undefined" || ["indexes","elements"].indexOf(return_type) === -1){ - return_type = "indexes"; - } - if (!Array.isArray(filters)){ return []; } - var test = function(element, filter){ - var operators = { - "=": function(a,b){ return a === b; }, - "<": function(a,b){ return a < b; }, - "<=": function(a,b){ return a <= b; }, - ">": function(a,b){ return a > b; }, - ">=": function(a,b){ return a >= b; }, - "%": function(a,b){ return a % b; } - }; - if (!Array.isArray(filter)){ return false; } - if (filter.length === 2){ - return element[filter[0]] === filter[1]; - } else if (filter.length === 3 && operators[filter[1]]){ - return operators[filter[1]](element[filter[0]], filter[2]); - } else { - return false; - } - }; - var matches = []; - this.data.forEach(function(element, idx){ - var match = true; - filters.forEach(function(filter){ - if (!test(element, filter)){ match = false; } - }); - if (match){ matches.push(return_type === "indexes" ? idx : element); } - }); - return matches; -}; - -/** + LocusZoom.DataLayer.prototype.filter = function (filters, return_type) { + if (typeof return_type == 'undefined' || [ + 'indexes', + 'elements' + ].indexOf(return_type) === -1) { + return_type = 'indexes'; + } + if (!Array.isArray(filters)) { + return []; + } + var test = function (element, filter) { + var operators = { + '=': function (a, b) { + return a === b; + }, + '<': function (a, b) { + return a < b; + }, + '<=': function (a, b) { + return a <= b; + }, + '>': function (a, b) { + return a > b; + }, + '>=': function (a, b) { + return a >= b; + }, + '%': function (a, b) { + return a % b; + } + }; + if (!Array.isArray(filter)) { + return false; + } + if (filter.length === 2) { + return element[filter[0]] === filter[1]; + } else if (filter.length === 3 && operators[filter[1]]) { + return operators[filter[1]](element[filter[0]], filter[2]); + } else { + return false; + } + }; + var matches = []; + this.data.forEach(function (element, idx) { + var match = true; + filters.forEach(function (filter) { + if (!test(element, filter)) { + match = false; + } + }); + if (match) { + matches.push(return_type === 'indexes' ? idx : element); + } + }); + return matches; + }; + /** * @param filters * @returns {Array} */ -LocusZoom.DataLayer.prototype.filterIndexes = function(filters){ return this.filter(filters, "indexes"); }; -/** + LocusZoom.DataLayer.prototype.filterIndexes = function (filters) { + return this.filter(filters, 'indexes'); + }; + /** * @param filters * @returns {Array} */ -LocusZoom.DataLayer.prototype.filterElements = function(filters){ return this.filter(filters, "elements"); }; - -LocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){ - var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx]; - var antiverb = "un" + verb; - // Set/unset a single element's status - // TODO: Improve documentation for dynamically generated methods/properties - LocusZoom.DataLayer.prototype[verb + "Element"] = function(element, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - this.setElementStatus(adjective, element, true, exclusive); - return this; - }; - LocusZoom.DataLayer.prototype[antiverb + "Element"] = function(element, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - this.setElementStatus(adjective, element, false, exclusive); - return this; - }; - // Set/unset status for arbitrarily many elements given a set of filters - LocusZoom.DataLayer.prototype[verb + "ElementsByFilters"] = function(filters, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - return this.setElementStatusByFilters(adjective, true, filters, exclusive); - }; - LocusZoom.DataLayer.prototype[antiverb + "ElementsByFilters"] = function(filters, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - return this.setElementStatusByFilters(adjective, false, filters, exclusive); - }; - // Set/unset status for all elements - LocusZoom.DataLayer.prototype[verb + "AllElements"] = function(){ - this.setAllElementStatus(adjective, true); - return this; - }; - LocusZoom.DataLayer.prototype[antiverb + "AllElements"] = function(){ - this.setAllElementStatus(adjective, false); - return this; - }; -}); - -/** + LocusZoom.DataLayer.prototype.filterElements = function (filters) { + return this.filter(filters, 'elements'); + }; + LocusZoom.DataLayer.Statuses.verbs.forEach(function (verb, idx) { + var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx]; + var antiverb = 'un' + verb; + // Set/unset a single element's status + // TODO: Improve documentation for dynamically generated methods/properties + LocusZoom.DataLayer.prototype[verb + 'Element'] = function (element, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + this.setElementStatus(adjective, element, true, exclusive); + return this; + }; + LocusZoom.DataLayer.prototype[antiverb + 'Element'] = function (element, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + this.setElementStatus(adjective, element, false, exclusive); + return this; + }; + // Set/unset status for arbitrarily many elements given a set of filters + LocusZoom.DataLayer.prototype[verb + 'ElementsByFilters'] = function (filters, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + return this.setElementStatusByFilters(adjective, true, filters, exclusive); + }; + LocusZoom.DataLayer.prototype[antiverb + 'ElementsByFilters'] = function (filters, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + return this.setElementStatusByFilters(adjective, false, filters, exclusive); + }; + // Set/unset status for all elements + LocusZoom.DataLayer.prototype[verb + 'AllElements'] = function () { + this.setAllElementStatus(adjective, true); + return this; + }; + LocusZoom.DataLayer.prototype[antiverb + 'AllElements'] = function () { + this.setAllElementStatus(adjective, false); + return this; + }; + }); + /** * Toggle a status (e.g. highlighted, selected, identified) on an element * @param {String} status * @param {String|Object} element @@ -2610,59 +2898,49 @@ LocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){ * @param {Boolean} exclusive * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.setElementStatus = function(status, element, toggle, exclusive){ - - // Sanity checks - if (typeof status == "undefined" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){ - throw("Invalid status passed to DataLayer.setElementStatus()"); - } - if (typeof element == "undefined"){ - throw("Invalid element passed to DataLayer.setElementStatus()"); - } - if (typeof toggle == "undefined"){ - toggle = true; - } - - // Get an ID for the element or return having changed nothing - try { - var element_id = this.getElementId(element); - } catch (get_element_id_error){ - return this; - } - - // Enforce exclusivity (force all elements to have the opposite of toggle first) - if (exclusive){ - this.setAllElementStatus(status, !toggle); - } - - // Set/unset the proper status class on the appropriate DOM element(s) - d3.select("#" + element_id).classed("lz-data_layer-" + this.layout.type + "-" + status, toggle); - var element_status_node_id = this.getElementStatusNodeId(element); - if (element_status_node_id !== null){ - d3.select("#" + element_status_node_id).classed("lz-data_layer-" + this.layout.type + "-statusnode-" + status, toggle); - } - - // Track element ID in the proper status state array - var element_status_idx = this.state[this.state_id][status].indexOf(element_id); - if (toggle && element_status_idx === -1){ - this.state[this.state_id][status].push(element_id); - } - if (!toggle && element_status_idx !== -1){ - this.state[this.state_id][status].splice(element_status_idx, 1); - } - - // Trigger tool tip show/hide logic - this.showOrHideTooltip(element); - - // Trigger layout changed event hook - this.parent.emit("layout_changed"); - this.parent_plot.emit("layout_changed"); - - return this; - -}; - -/** + LocusZoom.DataLayer.prototype.setElementStatus = function (status, element, toggle, exclusive) { + // Sanity checks + if (typeof status == 'undefined' || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1) { + throw 'Invalid status passed to DataLayer.setElementStatus()'; + } + if (typeof element == 'undefined') { + throw 'Invalid element passed to DataLayer.setElementStatus()'; + } + if (typeof toggle == 'undefined') { + toggle = true; + } + // Get an ID for the element or return having changed nothing + try { + var element_id = this.getElementId(element); + } catch (get_element_id_error) { + return this; + } + // Enforce exclusivity (force all elements to have the opposite of toggle first) + if (exclusive) { + this.setAllElementStatus(status, !toggle); + } + // Set/unset the proper status class on the appropriate DOM element(s) + d3.select('#' + element_id).classed('lz-data_layer-' + this.layout.type + '-' + status, toggle); + var element_status_node_id = this.getElementStatusNodeId(element); + if (element_status_node_id !== null) { + d3.select('#' + element_status_node_id).classed('lz-data_layer-' + this.layout.type + '-statusnode-' + status, toggle); + } + // Track element ID in the proper status state array + var element_status_idx = this.state[this.state_id][status].indexOf(element_id); + if (toggle && element_status_idx === -1) { + this.state[this.state_id][status].push(element_id); + } + if (!toggle && element_status_idx !== -1) { + this.state[this.state_id][status].splice(element_status_idx, 1); + } + // Trigger tool tip show/hide logic + this.showOrHideTooltip(element); + // Trigger layout changed event hook + this.parent.emit('layout_changed'); + this.parent_plot.emit('layout_changed'); + return this; + }; + /** * Toggle a status on elements in the data layer based on a set of filters * @param {String} status * @param {Boolean} toggle @@ -2670,81 +2948,90 @@ LocusZoom.DataLayer.prototype.setElementStatus = function(status, element, toggl * @param {Boolean} exclusive * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.setElementStatusByFilters = function(status, toggle, filters, exclusive){ - - // Sanity check - if (typeof status == "undefined" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){ - throw("Invalid status passed to DataLayer.setElementStatusByFilters()"); - } - if (typeof this.state[this.state_id][status] == "undefined"){ return this; } - if (typeof toggle == "undefined"){ toggle = true; } else { toggle = !!toggle; } - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - if (!Array.isArray(filters)){ filters = []; } - - // Enforce exclusivity (force all elements to have the opposite of toggle first) - if (exclusive){ - this.setAllElementStatus(status, !toggle); - } - - // Apply statuses - this.filterElements(filters).forEach(function(element){ - this.setElementStatus(status, element, toggle); - }.bind(this)); - - return this; -}; - -/** + LocusZoom.DataLayer.prototype.setElementStatusByFilters = function (status, toggle, filters, exclusive) { + // Sanity check + if (typeof status == 'undefined' || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1) { + throw 'Invalid status passed to DataLayer.setElementStatusByFilters()'; + } + if (typeof this.state[this.state_id][status] == 'undefined') { + return this; + } + if (typeof toggle == 'undefined') { + toggle = true; + } else { + toggle = !!toggle; + } + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + if (!Array.isArray(filters)) { + filters = []; + } + // Enforce exclusivity (force all elements to have the opposite of toggle first) + if (exclusive) { + this.setAllElementStatus(status, !toggle); + } + // Apply statuses + this.filterElements(filters).forEach(function (element) { + this.setElementStatus(status, element, toggle); + }.bind(this)); + return this; + }; + /** * Toggle a status on all elements in the data layer * @param {String} status * @param {Boolean} toggle * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.setAllElementStatus = function(status, toggle){ - - // Sanity check - if (typeof status == "undefined" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){ - throw("Invalid status passed to DataLayer.setAllElementStatus()"); - } - if (typeof this.state[this.state_id][status] == "undefined"){ return this; } - if (typeof toggle == "undefined"){ toggle = true; } - - // Apply statuses - if (toggle){ - this.data.forEach(function(element){ - this.setElementStatus(status, element, true); - }.bind(this)); - } else { - var status_ids = this.state[this.state_id][status].slice(); - status_ids.forEach(function(id){ - var element = this.getElementById(id); - if (typeof element == "object" && element !== null){ - this.setElementStatus(status, element, false); - } - }.bind(this)); - this.state[this.state_id][status] = []; - } - - // Update global status flag - this.global_statuses[status] = toggle; - - return this; -}; - -/** + LocusZoom.DataLayer.prototype.setAllElementStatus = function (status, toggle) { + // Sanity check + if (typeof status == 'undefined' || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1) { + throw 'Invalid status passed to DataLayer.setAllElementStatus()'; + } + if (typeof this.state[this.state_id][status] == 'undefined') { + return this; + } + if (typeof toggle == 'undefined') { + toggle = true; + } + // Apply statuses + if (toggle) { + this.data.forEach(function (element) { + this.setElementStatus(status, element, true); + }.bind(this)); + } else { + var status_ids = this.state[this.state_id][status].slice(); + status_ids.forEach(function (id) { + var element = this.getElementById(id); + if (typeof element == 'object' && element !== null) { + this.setElementStatus(status, element, false); + } + }.bind(this)); + this.state[this.state_id][status] = []; + } + // Update global status flag + this.global_statuses[status] = toggle; + return this; + }; + /** * Apply all layout-defined behaviors to a selection of elements with event handlers * @param {d3.selection} selection */ -LocusZoom.DataLayer.prototype.applyBehaviors = function(selection){ - if (typeof this.layout.behaviors != "object"){ return; } - Object.keys(this.layout.behaviors).forEach(function(directive){ - var event_match = /(click|mouseover|mouseout)/.exec(directive); - if (!event_match){ return; } - selection.on(event_match[0] + "." + directive, this.executeBehaviors(directive, this.layout.behaviors[directive])); - }.bind(this)); -}; - -/** + LocusZoom.DataLayer.prototype.applyBehaviors = function (selection) { + if (typeof this.layout.behaviors != 'object') { + return; + } + Object.keys(this.layout.behaviors).forEach(function (directive) { + var event_match = /(click|mouseover|mouseout)/.exec(directive); + if (!event_match) { + return; + } + selection.on(event_match[0] + '.' + directive, this.executeBehaviors(directive, this.layout.behaviors[directive])); + }.bind(this)); + }; + /** * Generate a function that executes an arbitrary list of behaviors on an element during an event * @param {String} directive The name of the event, as described in layout.behaviors for this datalayer * @param {Object} behaviors An object describing the behavior to attach to this single element @@ -2756,273 +3043,248 @@ LocusZoom.DataLayer.prototype.applyBehaviors = function(selection){ * @returns {function(this:LocusZoom.DataLayer)} Return a function that handles the event in context with the behavior * and the element- can be attached as an event listener */ -LocusZoom.DataLayer.prototype.executeBehaviors = function(directive, behaviors) { - - // Determine the required state of control and shift keys during the event - var requiredKeyStates = { - "ctrl": (directive.indexOf("ctrl") !== -1), - "shift": (directive.indexOf("shift") !== -1) - }; - - return function(element){ - - // Do nothing if the required control and shift key presses (or lack thereof) doesn't match the event - if (requiredKeyStates.ctrl !== !!d3.event.ctrlKey || requiredKeyStates.shift !== !!d3.event.shiftKey){ return; } - - // Loop through behaviors making each one go in succession - behaviors.forEach(function(behavior){ - - // Route first by the action, if defined - if (typeof behavior != "object" || behavior === null){ return; } - - switch (behavior.action){ - - // Set a status (set to true regardless of current status, optionally with exclusivity) - case "set": - this.setElementStatus(behavior.status, element, true, behavior.exclusive); - break; - - // Unset a status (set to false regardless of current status, optionally with exclusivity) - case "unset": - this.setElementStatus(behavior.status, element, false, behavior.exclusive); - break; - - // Toggle a status - case "toggle": - var current_status_boolean = (this.state[this.state_id][behavior.status].indexOf(this.getElementId(element)) !== -1); - var exclusive = behavior.exclusive && !current_status_boolean; - this.setElementStatus(behavior.status, element, !current_status_boolean, exclusive); - break; - - // Link to a dynamic URL - case "link": - if (typeof behavior.href == "string"){ - var url = LocusZoom.parseFields(element, behavior.href); - if (typeof behavior.target == "string"){ - window.open(url, behavior.target); - } else { - window.location.href = url; - } + LocusZoom.DataLayer.prototype.executeBehaviors = function (directive, behaviors) { + // Determine the required state of control and shift keys during the event + var requiredKeyStates = { + 'ctrl': directive.indexOf('ctrl') !== -1, + 'shift': directive.indexOf('shift') !== -1 + }; + return function (element) { + // Do nothing if the required control and shift key presses (or lack thereof) doesn't match the event + if (requiredKeyStates.ctrl !== !!d3.event.ctrlKey || requiredKeyStates.shift !== !!d3.event.shiftKey) { + return; } - break; - - // Action not defined, just return - default: - break; - - } - - return; - - }.bind(this)); - - }.bind(this); - -}; - -/** + // Loop through behaviors making each one go in succession + behaviors.forEach(function (behavior) { + // Route first by the action, if defined + if (typeof behavior != 'object' || behavior === null) { + return; + } + switch (behavior.action) { + // Set a status (set to true regardless of current status, optionally with exclusivity) + case 'set': + this.setElementStatus(behavior.status, element, true, behavior.exclusive); + break; + // Unset a status (set to false regardless of current status, optionally with exclusivity) + case 'unset': + this.setElementStatus(behavior.status, element, false, behavior.exclusive); + break; + // Toggle a status + case 'toggle': + var current_status_boolean = this.state[this.state_id][behavior.status].indexOf(this.getElementId(element)) !== -1; + var exclusive = behavior.exclusive && !current_status_boolean; + this.setElementStatus(behavior.status, element, !current_status_boolean, exclusive); + break; + // Link to a dynamic URL + case 'link': + if (typeof behavior.href == 'string') { + var url = LocusZoom.parseFields(element, behavior.href); + if (typeof behavior.target == 'string') { + window.open(url, behavior.target); + } else { + window.location.href = url; + } + } + break; + // Action not defined, just return + default: + break; + } + return; + }.bind(this)); + }.bind(this); + }; + /** * Get an object with the x and y coordinates of the panel's origin in terms of the entire page * Necessary for positioning any HTML elements over the panel * @returns {{x: Number, y: Number}} */ -LocusZoom.DataLayer.prototype.getPageOrigin = function(){ - var panel_origin = this.parent.getPageOrigin(); - return { - x: panel_origin.x + this.parent.layout.margin.left, - y: panel_origin.y + this.parent.layout.margin.top - }; -}; - -/** + LocusZoom.DataLayer.prototype.getPageOrigin = function () { + var panel_origin = this.parent.getPageOrigin(); + return { + x: panel_origin.x + this.parent.layout.margin.left, + y: panel_origin.y + this.parent.layout.margin.top + }; + }; + /** * Get a data layer's current underlying data in a standard format (e.g. JSON or CSV) * @param {('csv'|'tsv'|'json')} format How to export the data * @returns {*} */ -LocusZoom.DataLayer.prototype.exportData = function(format){ - var default_format = "json"; - format = format || default_format; - format = (typeof format == "string" ? format.toLowerCase() : default_format); - if (["json","csv","tsv"].indexOf(format) === -1){ format = default_format; } - var ret; - switch (format){ - case "json": - try { - ret = JSON.stringify(this.data); - } catch (e){ - ret = null; - console.error("Unable to export JSON data from data layer: " + this.getBaseId() + ";", e); - } - break; - case "tsv": - case "csv": - try { - var jsonified = JSON.parse(JSON.stringify(this.data)); - if (typeof jsonified != "object"){ - ret = jsonified.toString(); - } else if (!Array.isArray(jsonified)){ - ret = "Object"; - } else { - var delimiter = (format === "tsv") ? "\t" : ","; - var header = this.layout.fields.map(function(header){ - return JSON.stringify(header); - }).join(delimiter) + "\n"; - ret = header + jsonified.map(function(record){ - return this.layout.fields.map(function(field){ - if (typeof record[field] == "undefined"){ - return JSON.stringify(null); - } else if (typeof record[field] == "object" && record[field] !== null){ - return Array.isArray(record[field]) ? "\"[Array(" + record[field].length + ")]\"" : "\"[Object]\""; - } else { - return JSON.stringify(record[field]); - } - }).join(delimiter); - }.bind(this)).join("\n"); + LocusZoom.DataLayer.prototype.exportData = function (format) { + var default_format = 'json'; + format = format || default_format; + format = typeof format == 'string' ? format.toLowerCase() : default_format; + if ([ + 'json', + 'csv', + 'tsv' + ].indexOf(format) === -1) { + format = default_format; } - } catch (e){ - ret = null; - console.error("Unable to export CSV data from data layer: " + this.getBaseId() + ";", e); - } - break; - } - return ret; -}; - -/** + var ret; + switch (format) { + case 'json': + try { + ret = JSON.stringify(this.data); + } catch (e) { + ret = null; + console.error('Unable to export JSON data from data layer: ' + this.getBaseId() + ';', e); + } + break; + case 'tsv': + case 'csv': + try { + var jsonified = JSON.parse(JSON.stringify(this.data)); + if (typeof jsonified != 'object') { + ret = jsonified.toString(); + } else if (!Array.isArray(jsonified)) { + ret = 'Object'; + } else { + var delimiter = format === 'tsv' ? '\t' : ','; + var header = this.layout.fields.map(function (header) { + return JSON.stringify(header); + }).join(delimiter) + '\n'; + ret = header + jsonified.map(function (record) { + return this.layout.fields.map(function (field) { + if (typeof record[field] == 'undefined') { + return JSON.stringify(null); + } else if (typeof record[field] == 'object' && record[field] !== null) { + return Array.isArray(record[field]) ? '"[Array(' + record[field].length + ')]"' : '"[Object]"'; + } else { + return JSON.stringify(record[field]); + } + }).join(delimiter); + }.bind(this)).join('\n'); + } + } catch (e) { + ret = null; + console.error('Unable to export CSV data from data layer: ' + this.getBaseId() + ';', e); + } + break; + } + return ret; + }; + /** * Position the datalayer and all tooltips * @returns {LocusZoom.DataLayer} */ -LocusZoom.DataLayer.prototype.draw = function(){ - this.svg.container.attr("transform", "translate(" + this.parent.layout.cliparea.origin.x + "," + this.parent.layout.cliparea.origin.y + ")"); - this.svg.clipRect - .attr("width", this.parent.layout.cliparea.width) - .attr("height", this.parent.layout.cliparea.height); - this.positionAllTooltips(); - return this; -}; - - -/** - * Re-Map a data layer to reflect changes in the state of a plot (such as viewing region/ chromosome range) + LocusZoom.DataLayer.prototype.draw = function () { + this.svg.container.attr('transform', 'translate(' + this.parent.layout.cliparea.origin.x + ',' + this.parent.layout.cliparea.origin.y + ')'); + this.svg.clipRect.attr('width', this.parent.layout.cliparea.width).attr('height', this.parent.layout.cliparea.height); + this.positionAllTooltips(); + return this; + }; + /** + * Re-Map a data layer to reflect changes in the state of a plot (such as viewing region/ chromosome range) * @return {Promise} */ -LocusZoom.DataLayer.prototype.reMap = function(){ - - this.destroyAllTooltips(); // hack - only non-visible tooltips should be destroyed - // and then recreated if returning to visibility - - // Fetch new data - var promise = this.parent_plot.lzd.getData(this.state, this.layout.fields); //,"ld:best" - promise.then(function(new_data){ - this.data = new_data.body; - this.applyDataMethods(); - this.initialized = true; - }.bind(this)); - - return promise; - -}; - - -/** + LocusZoom.DataLayer.prototype.reMap = function () { + this.destroyAllTooltips(); + // hack - only non-visible tooltips should be destroyed + // and then recreated if returning to visibility + // Fetch new data + var promise = this.parent_plot.lzd.getData(this.state, this.layout.fields); + //,"ld:best" + promise.then(function (new_data) { + this.data = new_data.body; + this.applyDataMethods(); + this.initialized = true; + }.bind(this)); + return promise; + }; + /** * The central registry of known data layer definitions (which may be stored in separate files due to length) * @namespace */ -LocusZoom.DataLayers = (function() { - var obj = {}; - var datalayers = {}; - /** + LocusZoom.DataLayers = function () { + var obj = {}; + var datalayers = {}; + /** * @name LocusZoom.DataLayers.get * @param {String} name The name of the datalayer * @param {Object} layout The configuration object for this data layer * @param {LocusZoom.DataLayer|LocusZoom.Panel} parent Where this layout is used * @returns {LocusZoom.DataLayer} */ - obj.get = function(name, layout, parent) { - if (!name) { - return null; - } else if (datalayers[name]) { - if (typeof layout != "object"){ - throw("invalid layout argument for data layer [" + name + "]"); - } else { - return new datalayers[name](layout, parent); - } - } else { - throw("data layer [" + name + "] not found"); - } - }; - - /** + obj.get = function (name, layout, parent) { + if (!name) { + return null; + } else if (datalayers[name]) { + if (typeof layout != 'object') { + throw 'invalid layout argument for data layer [' + name + ']'; + } else { + return new datalayers[name](layout, parent); + } + } else { + throw 'data layer [' + name + '] not found'; + } + }; + /** * @name LocusZoom.DataLayers.set * @protected * @param {String} name * @param {Function} datalayer Constructor for the datalayer */ - obj.set = function(name, datalayer) { - if (datalayer) { - if (typeof datalayer != "function"){ - throw("unable to set data layer [" + name + "], argument provided is not a function"); - } else { - datalayers[name] = datalayer; - datalayers[name].prototype = new LocusZoom.DataLayer(); - } - } else { - delete datalayers[name]; - } - }; - - /** + obj.set = function (name, datalayer) { + if (datalayer) { + if (typeof datalayer != 'function') { + throw 'unable to set data layer [' + name + '], argument provided is not a function'; + } else { + datalayers[name] = datalayer; + datalayers[name].prototype = new LocusZoom.DataLayer(); + } + } else { + delete datalayers[name]; + } + }; + /** * Add a new type of datalayer to the registry of known layer types * @name LocusZoom.DataLayers.add * @param {String} name The name of the data layer to register * @param {Function} datalayer */ - obj.add = function(name, datalayer) { - if (datalayers[name]) { - throw("data layer already exists with name: " + name); - } else { - obj.set(name, datalayer); - } - }; - - /** + obj.add = function (name, datalayer) { + if (datalayers[name]) { + throw 'data layer already exists with name: ' + name; + } else { + obj.set(name, datalayer); + } + }; + /** * Register a new datalayer that inherits and extends basic behaviors from a known datalayer * @param {String} parent_name The name of the parent data layer whose behavior is to be extended * @param {String} name The name of the new datalayer to register * @param {Object} [overrides] Object of properties and methods to combine with the prototype of the parent datalayer * @returns {Function} The constructor for the new child class */ - obj.extend = function(parent_name, name, overrides) { - // TODO: Consider exposing additional constructor argument, if there is a use case for very granular extension - overrides = overrides || {}; - - var parent = datalayers[parent_name]; - if (!parent) { - throw "Attempted to subclass an unknown or unregistered datalayer type"; - } - if (typeof overrides !== "object") { - throw "Must specify an object of properties and methods"; - } - var child = LocusZoom.subclass(parent, overrides); - // Bypass .set() because we want a layer of inheritance below `DataLayer` - datalayers[name] = child; - return child; - }; - - /** + obj.extend = function (parent_name, name, overrides) { + // TODO: Consider exposing additional constructor argument, if there is a use case for very granular extension + overrides = overrides || {}; + var parent = datalayers[parent_name]; + if (!parent) { + throw 'Attempted to subclass an unknown or unregistered datalayer type'; + } + if (typeof overrides !== 'object') { + throw 'Must specify an object of properties and methods'; + } + var child = LocusZoom.subclass(parent, overrides); + // Bypass .set() because we want a layer of inheritance below `DataLayer` + datalayers[name] = child; + return child; + }; + /** * List the names of all known datalayers * @name LocusZoom.DataLayers.list * @returns {String[]} */ - obj.list = function() { - return Object.keys(datalayers); - }; - - return obj; -})(); - -"use strict"; - -/** + obj.list = function () { + return Object.keys(datalayers); + }; + return obj; + }(); + 'use strict'; + /** * Create a single continuous 2D track that provides information about each datapoint * * For example, this can be used to color by membership in a group, alongside information in other panels @@ -3034,2229 +3296,1905 @@ LocusZoom.DataLayers = (function() { * @param {Array[]} An array of filter entries specifying which points to draw annotations for. * See `LocusZoom.DataLayer.filter` for details */ -LocusZoom.DataLayers.add("annotation_track", function(layout) { - // In the future we may add additional options for controlling marker size/ shape, based on user feedback - this.DefaultLayout = { - color: "#000000", - filters: [] - }; - - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - if (!Array.isArray(layout.filters)) { - throw "Annotation track must specify array of filters for selecting points to annotate"; - } - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - this.render = function() { - var self = this; - // Only render points that currently satisfy all provided filter conditions. - var trackData = this.filter(this.layout.filters, "elements"); - - var selection = this.svg.group - .selectAll("rect.lz-data_layer-" + self.layout.type) - .data(trackData, function(d) { return d[self.layout.id_field]; }); - - // Add new elements as needed - selection.enter() - .append("rect") - .attr("class", "lz-data_layer-" + this.layout.type) - .attr("id", function (d){ return self.getElementId(d); }); - // Update the set of elements to reflect new data - selection - .attr("x", function (d) { return self.parent["x_scale"](d[self.layout.x_axis.field]); }) - .attr("width", 1) // TODO autocalc width of track? Based on datarange / pixel width presumably - .attr("height", self.parent.layout.height) - .attr("fill", function(d){ return self.resolveScalableParameter(self.layout.color, d); }); - // Remove unused elements - selection.exit().remove(); - - // Set up tooltips and mouse interaction - this.applyBehaviors(selection); - }; - - // Reimplement the positionTooltip() method to be annotation-specific - this.positionTooltip = function(id) { - if (typeof id != "string") { - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]) { - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var top, left, arrow_type, arrow_top, arrow_left; - var tooltip = this.tooltips[id]; - var arrow_width = 7; // as defined in the default stylesheet - var stroke_width = 1; // as defined in the default stylesheet - var offset = stroke_width / 2; - var page_origin = this.getPageOrigin(); - - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); - - var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); - var y_center = data_layer_height / 2; - - // Tooltip should be horizontally centered above the point to be annotated. (or below if space is limited) - var offset_right = Math.max((tooltip_box.width / 2) - x_center, 0); - var offset_left = Math.max((tooltip_box.width / 2) + x_center - data_layer_width, 0); - left = page_origin.x + x_center - (tooltip_box.width / 2) - offset_left + offset_right; - arrow_left = (tooltip_box.width / 2) - (arrow_width) + offset_left - offset_right - offset; - if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - y_center) { - top = page_origin.y + y_center - (tooltip_box.height + stroke_width + arrow_width); - arrow_type = "down"; - arrow_top = tooltip_box.height - stroke_width; - } else { - top = page_origin.y + y_center + stroke_width + arrow_width; - arrow_type = "up"; - arrow_top = 0 - stroke_width - arrow_width; - } - // Apply positions to the main div - tooltip.selector.style("left", left + "px").style("top", top + "px"); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow) { - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style("left", arrow_left + "px") - .style("top", arrow_top + "px"); - }; - - return this; -}); - -"use strict"; - -/********************* + LocusZoom.DataLayers.add('annotation_track', function (layout) { + // In the future we may add additional options for controlling marker size/ shape, based on user feedback + this.DefaultLayout = { + color: '#000000', + filters: [] + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + if (!Array.isArray(layout.filters)) { + throw 'Annotation track must specify array of filters for selecting points to annotate'; + } + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + this.render = function () { + var self = this; + // Only render points that currently satisfy all provided filter conditions. + var trackData = this.filter(this.layout.filters, 'elements'); + var selection = this.svg.group.selectAll('rect.lz-data_layer-' + self.layout.type).data(trackData, function (d) { + return d[self.layout.id_field]; + }); + // Add new elements as needed + selection.enter().append('rect').attr('class', 'lz-data_layer-' + this.layout.type).attr('id', function (d) { + return self.getElementId(d); + }); + // Update the set of elements to reflect new data + selection.attr('x', function (d) { + return self.parent['x_scale'](d[self.layout.x_axis.field]); + }).attr('width', 1) // TODO autocalc width of track? Based on datarange / pixel width presumably +.attr('height', self.parent.layout.height).attr('fill', function (d) { + return self.resolveScalableParameter(self.layout.color, d); + }); + // Remove unused elements + selection.exit().remove(); + // Set up tooltips and mouse interaction + this.applyBehaviors(selection); + }; + // Reimplement the positionTooltip() method to be annotation-specific + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var top, left, arrow_type, arrow_top, arrow_left; + var tooltip = this.tooltips[id]; + var arrow_width = 7; + // as defined in the default stylesheet + var stroke_width = 1; + // as defined in the default stylesheet + var offset = stroke_width / 2; + var page_origin = this.getPageOrigin(); + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); + var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); + var y_center = data_layer_height / 2; + // Tooltip should be horizontally centered above the point to be annotated. (or below if space is limited) + var offset_right = Math.max(tooltip_box.width / 2 - x_center, 0); + var offset_left = Math.max(tooltip_box.width / 2 + x_center - data_layer_width, 0); + left = page_origin.x + x_center - tooltip_box.width / 2 - offset_left + offset_right; + arrow_left = tooltip_box.width / 2 - arrow_width + offset_left - offset_right - offset; + if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - y_center) { + top = page_origin.y + y_center - (tooltip_box.height + stroke_width + arrow_width); + arrow_type = 'down'; + arrow_top = tooltip_box.height - stroke_width; + } else { + top = page_origin.y + y_center + stroke_width + arrow_width; + arrow_type = 'up'; + arrow_top = 0 - stroke_width - arrow_width; + } + // Apply positions to the main div + tooltip.selector.style('left', left + 'px').style('top', top + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style('left', arrow_left + 'px').style('top', arrow_top + 'px'); + }; + return this; + }); + 'use strict'; + /********************* Forest Data Layer Implements a standard forest plot */ - -LocusZoom.DataLayers.add("forest", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - this.DefaultLayout = { - point_size: 40, - point_shape: "square", - color: "#888888", - fill_opacity: 1, - y_axis: { - axis: 2 - }, - id_field: "id", - confidence_intervals: { - start_field: "ci_start", - end_field: "ci_end" - }, - show_no_significance_line: true - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - // Reimplement the positionTooltip() method to be forest-specific - this.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]){ - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var tooltip = this.tooltips[id]; - var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data); - var arrow_width = 7; // as defined in the default stylesheet - var stroke_width = 1; // as defined in the default stylesheet - var border_radius = 6; // as defined in the default stylesheet - var page_origin = this.getPageOrigin(); - var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); - var y_scale = "y"+this.layout.y_axis.axis+"_scale"; - var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]); - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - // Position horizontally on the left or the right depending on which side of the plot the point is on - var offset = Math.sqrt(point_size / Math.PI); - var left, arrow_type, arrow_left; - if (x_center <= this.parent.layout.width / 2){ - left = page_origin.x + x_center + offset + arrow_width + stroke_width; - arrow_type = "left"; - arrow_left = -1 * (arrow_width + stroke_width); - } else { - left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width; - arrow_type = "right"; - arrow_left = tooltip_box.width - stroke_width; - } - // Position vertically centered unless we're at the top or bottom of the plot - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var top, arrow_top; - if (y_center - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down - top = page_origin.y + y_center - (1.5 * arrow_width) - border_radius; - arrow_top = border_radius; - } else if (y_center + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up - top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height; - arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius; - } else { // vertically centered - top = page_origin.y + y_center - (tooltip_box.height / 2); - arrow_top = (tooltip_box.height / 2) - arrow_width; - } - // Apply positions to the main div - tooltip.selector.style("left", left + "px").style("top", top + "px"); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow){ - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style("left", arrow_left + "px") - .style("top", arrow_top + "px"); - }; - - // Implement the main render function - this.render = function(){ - - var x_scale = "x_scale"; - var y_scale = "y"+this.layout.y_axis.axis+"_scale"; - - // Generate confidence interval paths if fields are defined - if (this.layout.confidence_intervals - && this.layout.fields.indexOf(this.layout.confidence_intervals.start_field) !== -1 - && this.layout.fields.indexOf(this.layout.confidence_intervals.end_field) !== -1){ - // Generate a selection for all forest plot confidence intervals - var ci_selection = this.svg.group - .selectAll("rect.lz-data_layer-forest.lz-data_layer-forest-ci") - .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this)); - // Create confidence interval rect elements - ci_selection.enter() - .append("rect") - .attr("class", "lz-data_layer-forest lz-data_layer-forest-ci") - .attr("id", function(d){ return this.getElementId(d) + "_ci"; }.bind(this)) - .attr("transform", "translate(0," + (isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height) + ")"); - // Apply position and size parameters using transition if necessary - var ci_transform = function(d) { - var x = this.parent[x_scale](d[this.layout.confidence_intervals.start_field]); - var y = this.parent[y_scale](d[this.layout.y_axis.field]); - if (isNaN(x)){ x = -1000; } - if (isNaN(y)){ y = -1000; } - return "translate(" + x + "," + y + ")"; - }.bind(this); - var ci_width = function(d){ - return this.parent[x_scale](d[this.layout.confidence_intervals.end_field]) - - this.parent[x_scale](d[this.layout.confidence_intervals.start_field]); - }.bind(this); - var ci_height = 1; - if (this.canTransition()){ - ci_selection - .transition() - .duration(this.layout.transition.duration || 0) - .ease(this.layout.transition.ease || "cubic-in-out") - .attr("transform", ci_transform) - .attr("width", ci_width).attr("height", ci_height); - } else { - ci_selection - .attr("transform", ci_transform) - .attr("width", ci_width).attr("height", ci_height); - } - // Remove old elements as needed - ci_selection.exit().remove(); - } - - // Generate a selection for all forest plot points - var points_selection = this.svg.group - .selectAll("path.lz-data_layer-forest.lz-data_layer-forest-point") - .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this)); - - // Create elements, apply class, ID, and initial position - var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height; - points_selection.enter() - .append("path") - .attr("class", "lz-data_layer-forest lz-data_layer-forest-point") - .attr("id", function(d){ return this.getElementId(d) + "_point"; }.bind(this)) - .attr("transform", "translate(0," + initial_y + ")"); - - // Generate new values (or functions for them) for position, color, size, and shape - var transform = function(d) { - var x = this.parent[x_scale](d[this.layout.x_axis.field]); - var y = this.parent[y_scale](d[this.layout.y_axis.field]); - if (isNaN(x)){ x = -1000; } - if (isNaN(y)){ y = -1000; } - return "translate(" + x + "," + y + ")"; - }.bind(this); - - var fill = function(d){ return this.resolveScalableParameter(this.layout.color, d); }.bind(this); - var fill_opacity = function(d){ return this.resolveScalableParameter(this.layout.fill_opacity, d); }.bind(this); - - var shape = d3.svg.symbol() - .size(function(d){ return this.resolveScalableParameter(this.layout.point_size, d); }.bind(this)) - .type(function(d){ return this.resolveScalableParameter(this.layout.point_shape, d); }.bind(this)); - - // Apply position and color, using a transition if necessary - if (this.canTransition()){ - points_selection - .transition() - .duration(this.layout.transition.duration || 0) - .ease(this.layout.transition.ease || "cubic-in-out") - .attr("transform", transform) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity) - .attr("d", shape); - } else { - points_selection - .attr("transform", transform) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity) - .attr("d", shape); - } - - // Remove old elements as needed - points_selection.exit().remove(); - - // Apply default event emitters to selection - points_selection.on("click.event_emitter", function(element){ - this.parent.emit("element_clicked", element); - this.parent_plot.emit("element_clicked", element); - }.bind(this)); - - // Apply behaviors to points - this.applyBehaviors(points_selection); - - }; - - return this; - -}); - -"use strict"; - -/********************* + LocusZoom.DataLayers.add('forest', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + this.DefaultLayout = { + point_size: 40, + point_shape: 'square', + color: '#888888', + fill_opacity: 1, + y_axis: { axis: 2 }, + id_field: 'id', + confidence_intervals: { + start_field: 'ci_start', + end_field: 'ci_end' + }, + show_no_significance_line: true + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + // Reimplement the positionTooltip() method to be forest-specific + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var tooltip = this.tooltips[id]; + var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data); + var arrow_width = 7; + // as defined in the default stylesheet + var stroke_width = 1; + // as defined in the default stylesheet + var border_radius = 6; + // as defined in the default stylesheet + var page_origin = this.getPageOrigin(); + var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]); + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + // Position horizontally on the left or the right depending on which side of the plot the point is on + var offset = Math.sqrt(point_size / Math.PI); + var left, arrow_type, arrow_left; + if (x_center <= this.parent.layout.width / 2) { + left = page_origin.x + x_center + offset + arrow_width + stroke_width; + arrow_type = 'left'; + arrow_left = -1 * (arrow_width + stroke_width); + } else { + left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width; + arrow_type = 'right'; + arrow_left = tooltip_box.width - stroke_width; + } + // Position vertically centered unless we're at the top or bottom of the plot + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var top, arrow_top; + if (y_center - tooltip_box.height / 2 <= 0) { + // Too close to the top, push it down + top = page_origin.y + y_center - 1.5 * arrow_width - border_radius; + arrow_top = border_radius; + } else if (y_center + tooltip_box.height / 2 >= data_layer_height) { + // Too close to the bottom, pull it up + top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height; + arrow_top = tooltip_box.height - 2 * arrow_width - border_radius; + } else { + // vertically centered + top = page_origin.y + y_center - tooltip_box.height / 2; + arrow_top = tooltip_box.height / 2 - arrow_width; + } + // Apply positions to the main div + tooltip.selector.style('left', left + 'px').style('top', top + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style('left', arrow_left + 'px').style('top', arrow_top + 'px'); + }; + // Implement the main render function + this.render = function () { + var x_scale = 'x_scale'; + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + // Generate confidence interval paths if fields are defined + if (this.layout.confidence_intervals && this.layout.fields.indexOf(this.layout.confidence_intervals.start_field) !== -1 && this.layout.fields.indexOf(this.layout.confidence_intervals.end_field) !== -1) { + // Generate a selection for all forest plot confidence intervals + var ci_selection = this.svg.group.selectAll('rect.lz-data_layer-forest.lz-data_layer-forest-ci').data(this.data, function (d) { + return d[this.layout.id_field]; + }.bind(this)); + // Create confidence interval rect elements + ci_selection.enter().append('rect').attr('class', 'lz-data_layer-forest lz-data_layer-forest-ci').attr('id', function (d) { + return this.getElementId(d) + '_ci'; + }.bind(this)).attr('transform', 'translate(0,' + (isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height) + ')'); + // Apply position and size parameters using transition if necessary + var ci_transform = function (d) { + var x = this.parent[x_scale](d[this.layout.confidence_intervals.start_field]); + var y = this.parent[y_scale](d[this.layout.y_axis.field]); + if (isNaN(x)) { + x = -1000; + } + if (isNaN(y)) { + y = -1000; + } + return 'translate(' + x + ',' + y + ')'; + }.bind(this); + var ci_width = function (d) { + return this.parent[x_scale](d[this.layout.confidence_intervals.end_field]) - this.parent[x_scale](d[this.layout.confidence_intervals.start_field]); + }.bind(this); + var ci_height = 1; + if (this.canTransition()) { + ci_selection.transition().duration(this.layout.transition.duration || 0).ease(this.layout.transition.ease || 'cubic-in-out').attr('transform', ci_transform).attr('width', ci_width).attr('height', ci_height); + } else { + ci_selection.attr('transform', ci_transform).attr('width', ci_width).attr('height', ci_height); + } + // Remove old elements as needed + ci_selection.exit().remove(); + } + // Generate a selection for all forest plot points + var points_selection = this.svg.group.selectAll('path.lz-data_layer-forest.lz-data_layer-forest-point').data(this.data, function (d) { + return d[this.layout.id_field]; + }.bind(this)); + // Create elements, apply class, ID, and initial position + var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height; + points_selection.enter().append('path').attr('class', 'lz-data_layer-forest lz-data_layer-forest-point').attr('id', function (d) { + return this.getElementId(d) + '_point'; + }.bind(this)).attr('transform', 'translate(0,' + initial_y + ')'); + // Generate new values (or functions for them) for position, color, size, and shape + var transform = function (d) { + var x = this.parent[x_scale](d[this.layout.x_axis.field]); + var y = this.parent[y_scale](d[this.layout.y_axis.field]); + if (isNaN(x)) { + x = -1000; + } + if (isNaN(y)) { + y = -1000; + } + return 'translate(' + x + ',' + y + ')'; + }.bind(this); + var fill = function (d) { + return this.resolveScalableParameter(this.layout.color, d); + }.bind(this); + var fill_opacity = function (d) { + return this.resolveScalableParameter(this.layout.fill_opacity, d); + }.bind(this); + var shape = d3.svg.symbol().size(function (d) { + return this.resolveScalableParameter(this.layout.point_size, d); + }.bind(this)).type(function (d) { + return this.resolveScalableParameter(this.layout.point_shape, d); + }.bind(this)); + // Apply position and color, using a transition if necessary + if (this.canTransition()) { + points_selection.transition().duration(this.layout.transition.duration || 0).ease(this.layout.transition.ease || 'cubic-in-out').attr('transform', transform).attr('fill', fill).attr('fill-opacity', fill_opacity).attr('d', shape); + } else { + points_selection.attr('transform', transform).attr('fill', fill).attr('fill-opacity', fill_opacity).attr('d', shape); + } + // Remove old elements as needed + points_selection.exit().remove(); + // Apply default event emitters to selection + points_selection.on('click.event_emitter', function (element) { + this.parent.emit('element_clicked', element); + this.parent_plot.emit('element_clicked', element); + }.bind(this)); + // Apply behaviors to points + this.applyBehaviors(points_selection); + }; + return this; + }); + 'use strict'; + /********************* * Genes Data Layer * Implements a data layer that will render gene tracks * @class * @augments LocusZoom.DataLayer */ -LocusZoom.DataLayers.add("genes", function(layout){ - /** + LocusZoom.DataLayers.add('genes', function (layout) { + /** * Define a default layout for this DataLayer type and merge it with the passed argument * @protected * @member {Object} * */ - this.DefaultLayout = { - label_font_size: 12, - label_exon_spacing: 4, - exon_height: 16, - bounding_box_padding: 6, - track_vertical_spacing: 10, - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - /** + this.DefaultLayout = { + label_font_size: 12, + label_exon_spacing: 4, + exon_height: 16, + bounding_box_padding: 6, + track_vertical_spacing: 10 + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + /** * Generate a statusnode ID for a given element * @override * @returns {String} */ - this.getElementStatusNodeId = function(element){ - return this.getElementId(element) + "-statusnode"; - }; - - /** + this.getElementStatusNodeId = function (element) { + return this.getElementId(element) + '-statusnode'; + }; + /** * Helper function to sum layout values to derive total height for a single gene track * @returns {number} */ - this.getTrackHeight = function(){ - return 2 * this.layout.bounding_box_padding - + this.layout.label_font_size - + this.layout.label_exon_spacing - + this.layout.exon_height - + this.layout.track_vertical_spacing; - }; - - /** + this.getTrackHeight = function () { + return 2 * this.layout.bounding_box_padding + this.layout.label_font_size + this.layout.label_exon_spacing + this.layout.exon_height + this.layout.track_vertical_spacing; + }; + /** * A gene may have arbitrarily many transcripts, but this data layer isn't set up to render them yet. * Stash a transcript_idx to point to the first transcript and use that for all transcript refs. * @member {number} * @type {number} */ - this.transcript_idx = 0; - - /** + this.transcript_idx = 0; + /** * An internal counter for the number of tracks in the data layer. Used as an internal counter for looping * over positions / assignments * @protected * @member {number} */ - this.tracks = 1; - - /** + this.tracks = 1; + /** * Store information about genes in dataset, in a hash indexed by track number: {track_number: [gene_indices]} * @member {Object.} */ - this.gene_track_index = { 1: [] }; - - /** + this.gene_track_index = { 1: [] }; + /** * Ensure that genes in overlapping chromosome regions are positioned so that parts of different genes do not * overlap in the view. A track is a row used to vertically separate overlapping genes. * @returns {LocusZoom.DataLayer} */ - this.assignTracks = function(){ - /** + this.assignTracks = function () { + /** * Function to get the width in pixels of a label given the text and layout attributes * TODO: Move to outer scope? * @param {String} gene_name * @param {number|string} font_size * @returns {number} */ - this.getLabelWidth = function(gene_name, font_size){ - try { - var temp_text = this.svg.group.append("text") - .attr("x", 0).attr("y", 0).attr("class", "lz-data_layer-genes lz-label") - .style("font-size", font_size) - .text(gene_name + "→"); - var label_width = temp_text.node().getBBox().width; - temp_text.remove(); - return label_width; - } catch (e){ - return 0; - } - }; - - // Reinitialize some metadata - this.tracks = 1; - this.gene_track_index = { 1: [] }; - - this.data.map(function(d, g){ - - // If necessary, split combined gene id / version fields into discrete fields. - // NOTE: this may be an issue with CSG's genes data source that may eventually be solved upstream. - if (this.data[g].gene_id && this.data[g].gene_id.indexOf(".")){ - var split = this.data[g].gene_id.split("."); - this.data[g].gene_id = split[0]; - this.data[g].gene_version = split[1]; - } - - // Stash the transcript ID on the parent gene - this.data[g].transcript_id = this.data[g].transcripts[this.transcript_idx].transcript_id; - - // Determine display range start and end, based on minimum allowable gene display width, bounded by what we can see - // (range: values in terms of pixels on the screen) - this.data[g].display_range = { - start: this.parent.x_scale(Math.max(d.start, this.state.start)), - end: this.parent.x_scale(Math.min(d.end, this.state.end)) - }; - this.data[g].display_range.label_width = this.getLabelWidth(this.data[g].gene_name, this.layout.label_font_size); - this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start; - // Determine label text anchor (default to middle) - this.data[g].display_range.text_anchor = "middle"; - if (this.data[g].display_range.width < this.data[g].display_range.label_width){ - if (d.start < this.state.start){ - this.data[g].display_range.end = this.data[g].display_range.start - + this.data[g].display_range.label_width - + this.layout.label_font_size; - this.data[g].display_range.text_anchor = "start"; - } else if (d.end > this.state.end){ - this.data[g].display_range.start = this.data[g].display_range.end - - this.data[g].display_range.label_width - - this.layout.label_font_size; - this.data[g].display_range.text_anchor = "end"; - } else { - var centered_margin = ((this.data[g].display_range.label_width - this.data[g].display_range.width) / 2) - + this.layout.label_font_size; - if ((this.data[g].display_range.start - centered_margin) < this.parent.x_scale(this.state.start)){ - this.data[g].display_range.start = this.parent.x_scale(this.state.start); - this.data[g].display_range.end = this.data[g].display_range.start + this.data[g].display_range.label_width; - this.data[g].display_range.text_anchor = "start"; - } else if ((this.data[g].display_range.end + centered_margin) > this.parent.x_scale(this.state.end)) { - this.data[g].display_range.end = this.parent.x_scale(this.state.end); - this.data[g].display_range.start = this.data[g].display_range.end - this.data[g].display_range.label_width; - this.data[g].display_range.text_anchor = "end"; - } else { - this.data[g].display_range.start -= centered_margin; - this.data[g].display_range.end += centered_margin; + this.getLabelWidth = function (gene_name, font_size) { + try { + var temp_text = this.svg.group.append('text').attr('x', 0).attr('y', 0).attr('class', 'lz-data_layer-genes lz-label').style('font-size', font_size).text(gene_name + '\u2192'); + var label_width = temp_text.node().getBBox().width; + temp_text.remove(); + return label_width; + } catch (e) { + return 0; } - } - this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start; - } - // Add bounding box padding to the calculated display range start, end, and width - this.data[g].display_range.start -= this.layout.bounding_box_padding; - this.data[g].display_range.end += this.layout.bounding_box_padding; - this.data[g].display_range.width += 2 * this.layout.bounding_box_padding; - // Convert and stash display range values into domain values - // (domain: values in terms of the data set, e.g. megabases) - this.data[g].display_domain = { - start: this.parent.x_scale.invert(this.data[g].display_range.start), - end: this.parent.x_scale.invert(this.data[g].display_range.end) - }; - this.data[g].display_domain.width = this.data[g].display_domain.end - this.data[g].display_domain.start; - - // Using display range/domain data generated above cast each gene to tracks such that none overlap - this.data[g].track = null; - var potential_track = 1; - while (this.data[g].track === null){ - var collision_on_potential_track = false; - this.gene_track_index[potential_track].map(function(placed_gene){ - if (!collision_on_potential_track){ - var min_start = Math.min(placed_gene.display_range.start, this.display_range.start); - var max_end = Math.max(placed_gene.display_range.end, this.display_range.end); - if ((max_end - min_start) < (placed_gene.display_range.width + this.display_range.width)){ - collision_on_potential_track = true; + }; + // Reinitialize some metadata + this.tracks = 1; + this.gene_track_index = { 1: [] }; + this.data.map(function (d, g) { + // If necessary, split combined gene id / version fields into discrete fields. + // NOTE: this may be an issue with CSG's genes data source that may eventually be solved upstream. + if (this.data[g].gene_id && this.data[g].gene_id.indexOf('.')) { + var split = this.data[g].gene_id.split('.'); + this.data[g].gene_id = split[0]; + this.data[g].gene_version = split[1]; + } + // Stash the transcript ID on the parent gene + this.data[g].transcript_id = this.data[g].transcripts[this.transcript_idx].transcript_id; + // Determine display range start and end, based on minimum allowable gene display width, bounded by what we can see + // (range: values in terms of pixels on the screen) + this.data[g].display_range = { + start: this.parent.x_scale(Math.max(d.start, this.state.start)), + end: this.parent.x_scale(Math.min(d.end, this.state.end)) + }; + this.data[g].display_range.label_width = this.getLabelWidth(this.data[g].gene_name, this.layout.label_font_size); + this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start; + // Determine label text anchor (default to middle) + this.data[g].display_range.text_anchor = 'middle'; + if (this.data[g].display_range.width < this.data[g].display_range.label_width) { + if (d.start < this.state.start) { + this.data[g].display_range.end = this.data[g].display_range.start + this.data[g].display_range.label_width + this.layout.label_font_size; + this.data[g].display_range.text_anchor = 'start'; + } else if (d.end > this.state.end) { + this.data[g].display_range.start = this.data[g].display_range.end - this.data[g].display_range.label_width - this.layout.label_font_size; + this.data[g].display_range.text_anchor = 'end'; + } else { + var centered_margin = (this.data[g].display_range.label_width - this.data[g].display_range.width) / 2 + this.layout.label_font_size; + if (this.data[g].display_range.start - centered_margin < this.parent.x_scale(this.state.start)) { + this.data[g].display_range.start = this.parent.x_scale(this.state.start); + this.data[g].display_range.end = this.data[g].display_range.start + this.data[g].display_range.label_width; + this.data[g].display_range.text_anchor = 'start'; + } else if (this.data[g].display_range.end + centered_margin > this.parent.x_scale(this.state.end)) { + this.data[g].display_range.end = this.parent.x_scale(this.state.end); + this.data[g].display_range.start = this.data[g].display_range.end - this.data[g].display_range.label_width; + this.data[g].display_range.text_anchor = 'end'; + } else { + this.data[g].display_range.start -= centered_margin; + this.data[g].display_range.end += centered_margin; + } } + this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start; } - }.bind(this.data[g])); - if (!collision_on_potential_track){ - this.data[g].track = potential_track; - this.gene_track_index[potential_track].push(this.data[g]); - } else { - potential_track++; - if (potential_track > this.tracks){ - this.tracks = potential_track; - this.gene_track_index[potential_track] = []; + // Add bounding box padding to the calculated display range start, end, and width + this.data[g].display_range.start -= this.layout.bounding_box_padding; + this.data[g].display_range.end += this.layout.bounding_box_padding; + this.data[g].display_range.width += 2 * this.layout.bounding_box_padding; + // Convert and stash display range values into domain values + // (domain: values in terms of the data set, e.g. megabases) + this.data[g].display_domain = { + start: this.parent.x_scale.invert(this.data[g].display_range.start), + end: this.parent.x_scale.invert(this.data[g].display_range.end) + }; + this.data[g].display_domain.width = this.data[g].display_domain.end - this.data[g].display_domain.start; + // Using display range/domain data generated above cast each gene to tracks such that none overlap + this.data[g].track = null; + var potential_track = 1; + while (this.data[g].track === null) { + var collision_on_potential_track = false; + this.gene_track_index[potential_track].map(function (placed_gene) { + if (!collision_on_potential_track) { + var min_start = Math.min(placed_gene.display_range.start, this.display_range.start); + var max_end = Math.max(placed_gene.display_range.end, this.display_range.end); + if (max_end - min_start < placed_gene.display_range.width + this.display_range.width) { + collision_on_potential_track = true; + } + } + }.bind(this.data[g])); + if (!collision_on_potential_track) { + this.data[g].track = potential_track; + this.gene_track_index[potential_track].push(this.data[g]); + } else { + potential_track++; + if (potential_track > this.tracks) { + this.tracks = potential_track; + this.gene_track_index[potential_track] = []; + } + } } - } - } - - // Stash parent references on all genes, trascripts, and exons - this.data[g].parent = this; - this.data[g].transcripts.map(function(d, t){ - this.data[g].transcripts[t].parent = this.data[g]; - this.data[g].transcripts[t].exons.map(function(d, e){ - this.data[g].transcripts[t].exons[e].parent = this.data[g].transcripts[t]; + // Stash parent references on all genes, trascripts, and exons + this.data[g].parent = this; + this.data[g].transcripts.map(function (d, t) { + this.data[g].transcripts[t].parent = this.data[g]; + this.data[g].transcripts[t].exons.map(function (d, e) { + this.data[g].transcripts[t].exons[e].parent = this.data[g].transcripts[t]; + }.bind(this)); + }.bind(this)); }.bind(this)); - }.bind(this)); - - }.bind(this)); - return this; - }; - - /** + return this; + }; + /** * Main render function */ - this.render = function(){ - - this.assignTracks(); - - var width, height, x, y; - - // Render gene groups - var selection = this.svg.group.selectAll("g.lz-data_layer-genes") - .data(this.data, function(d){ return d.gene_name; }); - - selection.enter().append("g") - .attr("class", "lz-data_layer-genes"); - - selection.attr("id", function(d){ return this.getElementId(d); }.bind(this)) - .each(function(gene){ - - var data_layer = gene.parent; - - // Render gene bounding boxes (status nodes to show selected/highlighted) - var bboxes = d3.select(this).selectAll("rect.lz-data_layer-genes.lz-data_layer-genes-statusnode") - .data([gene], function(d){ return data_layer.getElementStatusNodeId(d); }); - - bboxes.enter().append("rect") - .attr("class", "lz-data_layer-genes lz-data_layer-genes-statusnode"); - - bboxes - .attr("id", function(d){ + this.render = function () { + this.assignTracks(); + var width, height, x, y; + // Render gene groups + var selection = this.svg.group.selectAll('g.lz-data_layer-genes').data(this.data, function (d) { + return d.gene_name; + }); + selection.enter().append('g').attr('class', 'lz-data_layer-genes'); + selection.attr('id', function (d) { + return this.getElementId(d); + }.bind(this)).each(function (gene) { + var data_layer = gene.parent; + // Render gene bounding boxes (status nodes to show selected/highlighted) + var bboxes = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-data_layer-genes-statusnode').data([gene], function (d) { + return data_layer.getElementStatusNodeId(d); + }); + bboxes.enter().append('rect').attr('class', 'lz-data_layer-genes lz-data_layer-genes-statusnode'); + bboxes.attr('id', function (d) { return data_layer.getElementStatusNodeId(d); - }) - .attr("rx", function(){ + }).attr('rx', function () { return data_layer.layout.bounding_box_padding; - }) - .attr("ry", function(){ + }).attr('ry', function () { return data_layer.layout.bounding_box_padding; }); - - width = function(d){ - return d.display_range.width; - }; - height = function(){ - return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; - }; - x = function(d){ - return d.display_range.start; - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()); - }; - if (data_layer.canTransition()){ - bboxes - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } else { - bboxes - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } - - bboxes.exit().remove(); - - // Render gene boundaries - var boundaries = d3.select(this).selectAll("rect.lz-data_layer-genes.lz-boundary") - .data([gene], function(d){ return d.gene_name + "_boundary"; }); - - boundaries.enter().append("rect") - .attr("class", "lz-data_layer-genes lz-boundary"); - - width = function(d){ - return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start); - }; - height = function(){ - return 1; // TODO: scale dynamically? - }; - x = function(d){ - return data_layer.parent.x_scale(d.start); - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()) - + data_layer.layout.bounding_box_padding - + data_layer.layout.label_font_size - + data_layer.layout.label_exon_spacing - + (Math.max(data_layer.layout.exon_height, 3) / 2); - }; - if (data_layer.canTransition()){ - boundaries - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } else { - boundaries - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } - - boundaries.exit().remove(); - - // Render gene labels - var labels = d3.select(this).selectAll("text.lz-data_layer-genes.lz-label") - .data([gene], function(d){ return d.gene_name + "_label"; }); - - labels.enter().append("text") - .attr("class", "lz-data_layer-genes lz-label"); - - labels - .attr("text-anchor", function(d){ + width = function (d) { + return d.display_range.width; + }; + height = function () { + return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; + }; + x = function (d) { + return d.display_range.start; + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight(); + }; + if (data_layer.canTransition()) { + bboxes.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + bboxes.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + bboxes.exit().remove(); + // Render gene boundaries + var boundaries = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-boundary').data([gene], function (d) { + return d.gene_name + '_boundary'; + }); + boundaries.enter().append('rect').attr('class', 'lz-data_layer-genes lz-boundary'); + width = function (d) { + return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start); + }; + height = function () { + return 1; // TODO: scale dynamically? + }; + x = function (d) { + return data_layer.parent.x_scale(d.start); + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight() + data_layer.layout.bounding_box_padding + data_layer.layout.label_font_size + data_layer.layout.label_exon_spacing + Math.max(data_layer.layout.exon_height, 3) / 2; + }; + if (data_layer.canTransition()) { + boundaries.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + boundaries.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + boundaries.exit().remove(); + // Render gene labels + var labels = d3.select(this).selectAll('text.lz-data_layer-genes.lz-label').data([gene], function (d) { + return d.gene_name + '_label'; + }); + labels.enter().append('text').attr('class', 'lz-data_layer-genes lz-label'); + labels.attr('text-anchor', function (d) { return d.display_range.text_anchor; - }) - .text(function(d){ - return (d.strand === "+") ? d.gene_name + "→" : "←" + d.gene_name; - }) - .style("font-size", gene.parent.layout.label_font_size); - - x = function(d){ - if (d.display_range.text_anchor === "middle"){ - return d.display_range.start + (d.display_range.width / 2); - } else if (d.display_range.text_anchor === "start"){ - return d.display_range.start + data_layer.layout.bounding_box_padding; - } else if (d.display_range.text_anchor === "end"){ - return d.display_range.end - data_layer.layout.bounding_box_padding; + }).text(function (d) { + return d.strand === '+' ? d.gene_name + '\u2192' : '\u2190' + d.gene_name; + }).style('font-size', gene.parent.layout.label_font_size); + x = function (d) { + if (d.display_range.text_anchor === 'middle') { + return d.display_range.start + d.display_range.width / 2; + } else if (d.display_range.text_anchor === 'start') { + return d.display_range.start + data_layer.layout.bounding_box_padding; + } else if (d.display_range.text_anchor === 'end') { + return d.display_range.end - data_layer.layout.bounding_box_padding; + } + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight() + data_layer.layout.bounding_box_padding + data_layer.layout.label_font_size; + }; + if (data_layer.canTransition()) { + labels.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('x', x).attr('y', y); + } else { + labels.attr('x', x).attr('y', y); } - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()) - + data_layer.layout.bounding_box_padding - + data_layer.layout.label_font_size; - }; - if (data_layer.canTransition()){ - labels - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("x", x).attr("y", y); - } else { - labels - .attr("x", x).attr("y", y); - } - - labels.exit().remove(); - - // Render exon rects (first transcript only, for now) - var exons = d3.select(this).selectAll("rect.lz-data_layer-genes.lz-exon") - .data(gene.transcripts[gene.parent.transcript_idx].exons, function(d){ return d.exon_id; }); - - exons.enter().append("rect") - .attr("class", "lz-data_layer-genes lz-exon"); - - width = function(d){ - return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start); - }; - height = function(){ - return data_layer.layout.exon_height; - }; - x = function(d){ - return data_layer.parent.x_scale(d.start); - }; - y = function(){ - return ((gene.track-1) * data_layer.getTrackHeight()) - + data_layer.layout.bounding_box_padding - + data_layer.layout.label_font_size - + data_layer.layout.label_exon_spacing; - }; - if (data_layer.canTransition()){ - exons - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } else { - exons - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } - - exons.exit().remove(); - - // Render gene click area - var clickareas = d3.select(this).selectAll("rect.lz-data_layer-genes.lz-clickarea") - .data([gene], function(d){ return d.gene_name + "_clickarea"; }); - - clickareas.enter().append("rect") - .attr("class", "lz-data_layer-genes lz-clickarea"); - - clickareas - .attr("id", function(d){ - return data_layer.getElementId(d) + "_clickarea"; - }) - .attr("rx", function(){ + labels.exit().remove(); + // Render exon rects (first transcript only, for now) + var exons = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-exon').data(gene.transcripts[gene.parent.transcript_idx].exons, function (d) { + return d.exon_id; + }); + exons.enter().append('rect').attr('class', 'lz-data_layer-genes lz-exon'); + width = function (d) { + return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start); + }; + height = function () { + return data_layer.layout.exon_height; + }; + x = function (d) { + return data_layer.parent.x_scale(d.start); + }; + y = function () { + return (gene.track - 1) * data_layer.getTrackHeight() + data_layer.layout.bounding_box_padding + data_layer.layout.label_font_size + data_layer.layout.label_exon_spacing; + }; + if (data_layer.canTransition()) { + exons.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + exons.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + exons.exit().remove(); + // Render gene click area + var clickareas = d3.select(this).selectAll('rect.lz-data_layer-genes.lz-clickarea').data([gene], function (d) { + return d.gene_name + '_clickarea'; + }); + clickareas.enter().append('rect').attr('class', 'lz-data_layer-genes lz-clickarea'); + clickareas.attr('id', function (d) { + return data_layer.getElementId(d) + '_clickarea'; + }).attr('rx', function () { return data_layer.layout.bounding_box_padding; - }) - .attr("ry", function(){ + }).attr('ry', function () { return data_layer.layout.bounding_box_padding; }); - - width = function(d){ - return d.display_range.width; - }; - height = function(){ - return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; - }; - x = function(d){ - return d.display_range.start; - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()); - }; - if (data_layer.canTransition()){ - clickareas - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } else { - clickareas - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } - - // Remove old clickareas as needed - clickareas.exit().remove(); - - // Apply default event emitters to clickareas - clickareas.on("click.event_emitter", function(element){ - element.parent.parent.emit("element_clicked", element); - element.parent.parent_plot.emit("element_clicked", element); + width = function (d) { + return d.display_range.width; + }; + height = function () { + return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; + }; + x = function (d) { + return d.display_range.start; + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight(); + }; + if (data_layer.canTransition()) { + clickareas.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + clickareas.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + // Remove old clickareas as needed + clickareas.exit().remove(); + // Apply default event emitters to clickareas + clickareas.on('click.event_emitter', function (element) { + element.parent.parent.emit('element_clicked', element); + element.parent.parent_plot.emit('element_clicked', element); + }); + // Apply mouse behaviors to clickareas + data_layer.applyBehaviors(clickareas); }); - - // Apply mouse behaviors to clickareas - data_layer.applyBehaviors(clickareas); - - }); - - // Remove old elements as needed - selection.exit().remove(); - - }; - - /** + // Remove old elements as needed + selection.exit().remove(); + }; + /** * Reimplement the positionTooltip() method to be gene-specific * @param {String} id */ - this.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]){ - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var tooltip = this.tooltips[id]; - var arrow_width = 7; // as defined in the default stylesheet - var stroke_width = 1; // as defined in the default stylesheet - var page_origin = this.getPageOrigin(); - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - var gene_bbox_id = this.getElementStatusNodeId(tooltip.data); - var gene_bbox = d3.select("#" + gene_bbox_id).node().getBBox(); - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); - // Position horizontally: attempt to center on the portion of the gene that's visible, - // pad to either side if bumping up against the edge of the data layer - var gene_center_x = ((tooltip.data.display_range.start + tooltip.data.display_range.end) / 2) - (this.layout.bounding_box_padding / 2); - var offset_right = Math.max((tooltip_box.width / 2) - gene_center_x, 0); - var offset_left = Math.max((tooltip_box.width / 2) + gene_center_x - data_layer_width, 0); - var left = page_origin.x + gene_center_x - (tooltip_box.width / 2) - offset_left + offset_right; - var arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right; - // Position vertically below the gene unless there's insufficient space - var top, arrow_type, arrow_top; - if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (gene_bbox.y + gene_bbox.height)){ - top = page_origin.y + gene_bbox.y - (tooltip_box.height + stroke_width + arrow_width); - arrow_type = "down"; - arrow_top = tooltip_box.height - stroke_width; - } else { - top = page_origin.y + gene_bbox.y + gene_bbox.height + stroke_width + arrow_width; - arrow_type = "up"; - arrow_top = 0 - stroke_width - arrow_width; - } - // Apply positions to the main div - tooltip.selector.style("left", left + "px").style("top", top + "px"); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow){ - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style("left", arrow_left + "px") - .style("top", arrow_top + "px"); - }; - - return this; - -}); - -"use strict"; - -/********************* + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var tooltip = this.tooltips[id]; + var arrow_width = 7; + // as defined in the default stylesheet + var stroke_width = 1; + // as defined in the default stylesheet + var page_origin = this.getPageOrigin(); + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + var gene_bbox_id = this.getElementStatusNodeId(tooltip.data); + var gene_bbox = d3.select('#' + gene_bbox_id).node().getBBox(); + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); + // Position horizontally: attempt to center on the portion of the gene that's visible, + // pad to either side if bumping up against the edge of the data layer + var gene_center_x = (tooltip.data.display_range.start + tooltip.data.display_range.end) / 2 - this.layout.bounding_box_padding / 2; + var offset_right = Math.max(tooltip_box.width / 2 - gene_center_x, 0); + var offset_left = Math.max(tooltip_box.width / 2 + gene_center_x - data_layer_width, 0); + var left = page_origin.x + gene_center_x - tooltip_box.width / 2 - offset_left + offset_right; + var arrow_left = tooltip_box.width / 2 - arrow_width / 2 + offset_left - offset_right; + // Position vertically below the gene unless there's insufficient space + var top, arrow_type, arrow_top; + if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (gene_bbox.y + gene_bbox.height)) { + top = page_origin.y + gene_bbox.y - (tooltip_box.height + stroke_width + arrow_width); + arrow_type = 'down'; + arrow_top = tooltip_box.height - stroke_width; + } else { + top = page_origin.y + gene_bbox.y + gene_bbox.height + stroke_width + arrow_width; + arrow_type = 'up'; + arrow_top = 0 - stroke_width - arrow_width; + } + // Apply positions to the main div + tooltip.selector.style('left', left + 'px').style('top', top + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style('left', arrow_left + 'px').style('top', arrow_top + 'px'); + }; + return this; + }); + 'use strict'; + /********************* Genome Legend Data Layer Implements a data layer that will render a genome legend */ - -// Build a custom data layer for a genome legend -LocusZoom.DataLayers.add("genome_legend", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - this.DefaultLayout = { - chromosome_fill_colors: { - light: "rgb(155, 155, 188)", - dark: "rgb(95, 95, 128)" - }, - chromosome_label_colors: { - light: "rgb(120, 120, 186)", - dark: "rgb(0, 0, 66)" - } - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - // Implement the main render function - this.render = function(){ - - // Iterate over data to generate genome-wide start/end values for each chromosome - var position = 0; - this.data.forEach(function(d, i){ - this.data[i].genome_start = position; - this.data[i].genome_end = position + d["genome:base_pairs"]; - position += d["genome:base_pairs"]; - }.bind(this)); - - var chromosomes = this.svg.group - .selectAll("rect.lz-data_layer-genome_legend") - .data(this.data, function(d){ return d["genome:chr"]; }); - - // Create chromosome elements, apply class - chromosomes.enter() - .append("rect") - .attr("class", "lz-data_layer-genome_legend"); - - // Position and fill chromosome rects - var data_layer = this; - var panel = this.parent; - - chromosomes - .attr("fill", function(d){ return (d["genome:chr"] % 2 ? data_layer.layout.chromosome_fill_colors.light : data_layer.layout.chromosome_fill_colors.dark); }) - .attr("x", function(d){ return panel.x_scale(d.genome_start); }) - .attr("y", 0) - .attr("width", function(d){ return panel.x_scale(d["genome:base_pairs"]); }) - .attr("height", panel.layout.cliparea.height); - - // Remove old elements as needed - chromosomes.exit().remove(); - - // Parse current state variant into a position - // Assumes that variant string is of the format 10:123352136_C/T or 10:123352136 - var variant_parts = /([^:]+):(\d+)(?:_.*)?/.exec(this.state.variant); - if (!variant_parts) { - throw("Genome legend cannot understand the specified variant position"); - } - var chr = variant_parts[1]; - var offset = variant_parts[2]; - // TODO: How does this handle representation of X or Y chromosomes? - position = +this.data[chr-1].genome_start + +offset; - - // Render the position - var region = this.svg.group - .selectAll("rect.lz-data_layer-genome_legend-marker") - .data([{ start: position, end: position + 1 }]); - - region.enter() - .append("rect") - .attr("class", "lz-data_layer-genome_legend-marker"); - - region - .transition() - .duration(500) - .style({ - "fill": "rgba(255, 250, 50, 0.8)", - "stroke": "rgba(255, 250, 50, 0.8)", - "stroke-width": "3px" - }) - .attr("x", function(d){ return panel.x_scale(d.start); }) - .attr("y", 0) - .attr("width", function(d){ return panel.x_scale(d.end - d.start); }) - .attr("height", panel.layout.cliparea.height); - - region.exit().remove(); - - }; - - return this; - -}); - -"use strict"; - -/** + // Build a custom data layer for a genome legend + LocusZoom.DataLayers.add('genome_legend', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + this.DefaultLayout = { + chromosome_fill_colors: { + light: 'rgb(155, 155, 188)', + dark: 'rgb(95, 95, 128)' + }, + chromosome_label_colors: { + light: 'rgb(120, 120, 186)', + dark: 'rgb(0, 0, 66)' + } + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + // Implement the main render function + this.render = function () { + // Iterate over data to generate genome-wide start/end values for each chromosome + var position = 0; + this.data.forEach(function (d, i) { + this.data[i].genome_start = position; + this.data[i].genome_end = position + d['genome:base_pairs']; + position += d['genome:base_pairs']; + }.bind(this)); + var chromosomes = this.svg.group.selectAll('rect.lz-data_layer-genome_legend').data(this.data, function (d) { + return d['genome:chr']; + }); + // Create chromosome elements, apply class + chromosomes.enter().append('rect').attr('class', 'lz-data_layer-genome_legend'); + // Position and fill chromosome rects + var data_layer = this; + var panel = this.parent; + chromosomes.attr('fill', function (d) { + return d['genome:chr'] % 2 ? data_layer.layout.chromosome_fill_colors.light : data_layer.layout.chromosome_fill_colors.dark; + }).attr('x', function (d) { + return panel.x_scale(d.genome_start); + }).attr('y', 0).attr('width', function (d) { + return panel.x_scale(d['genome:base_pairs']); + }).attr('height', panel.layout.cliparea.height); + // Remove old elements as needed + chromosomes.exit().remove(); + // Parse current state variant into a position + // Assumes that variant string is of the format 10:123352136_C/T or 10:123352136 + var variant_parts = /([^:]+):(\d+)(?:_.*)?/.exec(this.state.variant); + if (!variant_parts) { + throw 'Genome legend cannot understand the specified variant position'; + } + var chr = variant_parts[1]; + var offset = variant_parts[2]; + // TODO: How does this handle representation of X or Y chromosomes? + position = +this.data[chr - 1].genome_start + +offset; + // Render the position + var region = this.svg.group.selectAll('rect.lz-data_layer-genome_legend-marker').data([{ + start: position, + end: position + 1 + }]); + region.enter().append('rect').attr('class', 'lz-data_layer-genome_legend-marker'); + region.transition().duration(500).style({ + 'fill': 'rgba(255, 250, 50, 0.8)', + 'stroke': 'rgba(255, 250, 50, 0.8)', + 'stroke-width': '3px' + }).attr('x', function (d) { + return panel.x_scale(d.start); + }).attr('y', 0).attr('width', function (d) { + return panel.x_scale(d.end - d.start); + }).attr('height', panel.layout.cliparea.height); + region.exit().remove(); + }; + return this; + }); + 'use strict'; + /** * Intervals Data Layer * Implements a data layer that will render interval annotation tracks (intervals must provide start and end values) * @class LocusZoom.DataLayers.intervals * @augments LocusZoom.DataLayer */ -LocusZoom.DataLayers.add("intervals", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - this.DefaultLayout = { - start_field: "start", - end_field: "end", - track_split_field: "state_id", - track_split_order: "DESC", - track_split_legend_to_y_axis: 2, - split_tracks: true, - track_height: 15, - track_vertical_spacing: 3, - bounding_box_padding: 2, - always_hide_legend: false, - color: "#B8B8B8", - fill_opacity: 1 - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - /** + LocusZoom.DataLayers.add('intervals', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + this.DefaultLayout = { + start_field: 'start', + end_field: 'end', + track_split_field: 'state_id', + track_split_order: 'DESC', + track_split_legend_to_y_axis: 2, + split_tracks: true, + track_height: 15, + track_vertical_spacing: 3, + bounding_box_padding: 2, + always_hide_legend: false, + color: '#B8B8B8', + fill_opacity: 1 + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + /** * To define shared highlighting on the track split field define the status node id override * to generate an ID common to the track when we're actively splitting data out to separate tracks * @override * @returns {String} */ - this.getElementStatusNodeId = function(element){ - if (this.layout.split_tracks){ - return (this.getBaseId() + "-statusnode-" + element[this.layout.track_split_field]).replace(/[:.[\],]/g, "_"); - } - return this.getElementId(element) + "-statusnode"; - }.bind(this); - - // Helper function to sum layout values to derive total height for a single interval track - this.getTrackHeight = function(){ - return this.layout.track_height - + this.layout.track_vertical_spacing - + (2 * this.layout.bounding_box_padding); - }; - - this.tracks = 1; - this.previous_tracks = 1; - - // track-number-indexed object with arrays of interval indexes in the dataset - this.interval_track_index = { 1: [] }; - - // After we've loaded interval data interpret it to assign - // each to a track so that they do not overlap in the view - this.assignTracks = function(){ - - // Reinitialize some metadata - this.previous_tracks = this.tracks; - this.tracks = 0; - this.interval_track_index = { 1: [] }; - this.track_split_field_index = {}; - - // If splitting tracks by a field's value then do a first pass determine - // a value/track mapping that preserves the order of possible values - if (this.layout.track_split_field && this.layout.split_tracks){ - this.data.map(function(d){ - this.track_split_field_index[d[this.layout.track_split_field]] = null; - }.bind(this)); - var index = Object.keys(this.track_split_field_index); - if (this.layout.track_split_order === "DESC"){ index.reverse(); } - index.forEach(function(val){ - this.track_split_field_index[val] = this.tracks + 1; - this.interval_track_index[this.tracks + 1] = []; - this.tracks++; - }.bind(this)); - } - - this.data.map(function(d, i){ - - // Stash a parent reference on the interval - this.data[i].parent = this; - - // Determine display range start and end, based on minimum allowable interval display width, - // bounded by what we can see (range: values in terms of pixels on the screen) - this.data[i].display_range = { - start: this.parent.x_scale(Math.max(d[this.layout.start_field], this.state.start)), - end: this.parent.x_scale(Math.min(d[this.layout.end_field], this.state.end)) - }; - this.data[i].display_range.width = this.data[i].display_range.end - this.data[i].display_range.start; - - // Convert and stash display range values into domain values - // (domain: values in terms of the data set, e.g. megabases) - this.data[i].display_domain = { - start: this.parent.x_scale.invert(this.data[i].display_range.start), - end: this.parent.x_scale.invert(this.data[i].display_range.end) - }; - this.data[i].display_domain.width = this.data[i].display_domain.end - this.data[i].display_domain.start; - - // If splitting to tracks based on the value of the designated track split field - // then don't bother with collision detection (intervals will be grouped on tracks - // solely by the value of track_split_field) - if (this.layout.track_split_field && this.layout.split_tracks){ - var val = this.data[i][this.layout.track_split_field]; - this.data[i].track = this.track_split_field_index[val]; - this.interval_track_index[this.data[i].track].push(i); - } else { - // If not splitting to tracks based on a field value then do so based on collision - // detection (as how it's done for genes). Use display range/domain data generated - // above and cast each interval to tracks such that none overlap - this.tracks = 1; - this.data[i].track = null; - var potential_track = 1; - while (this.data[i].track === null){ - var collision_on_potential_track = false; - this.interval_track_index[potential_track].map(function(placed_interval){ - if (!collision_on_potential_track){ - var min_start = Math.min(placed_interval.display_range.start, this.display_range.start); - var max_end = Math.max(placed_interval.display_range.end, this.display_range.end); - if ((max_end - min_start) < (placed_interval.display_range.width + this.display_range.width)){ - collision_on_potential_track = true; - } - } - }.bind(this.data[i])); - if (!collision_on_potential_track){ - this.data[i].track = potential_track; - this.interval_track_index[potential_track].push(this.data[i]); + this.getElementStatusNodeId = function (element) { + if (this.layout.split_tracks) { + return (this.getBaseId() + '-statusnode-' + element[this.layout.track_split_field]).replace(/[:.[\],]/g, '_'); + } + return this.getElementId(element) + '-statusnode'; + }.bind(this); + // Helper function to sum layout values to derive total height for a single interval track + this.getTrackHeight = function () { + return this.layout.track_height + this.layout.track_vertical_spacing + 2 * this.layout.bounding_box_padding; + }; + this.tracks = 1; + this.previous_tracks = 1; + // track-number-indexed object with arrays of interval indexes in the dataset + this.interval_track_index = { 1: [] }; + // After we've loaded interval data interpret it to assign + // each to a track so that they do not overlap in the view + this.assignTracks = function () { + // Reinitialize some metadata + this.previous_tracks = this.tracks; + this.tracks = 0; + this.interval_track_index = { 1: [] }; + this.track_split_field_index = {}; + // If splitting tracks by a field's value then do a first pass determine + // a value/track mapping that preserves the order of possible values + if (this.layout.track_split_field && this.layout.split_tracks) { + this.data.map(function (d) { + this.track_split_field_index[d[this.layout.track_split_field]] = null; + }.bind(this)); + var index = Object.keys(this.track_split_field_index); + if (this.layout.track_split_order === 'DESC') { + index.reverse(); + } + index.forEach(function (val) { + this.track_split_field_index[val] = this.tracks + 1; + this.interval_track_index[this.tracks + 1] = []; + this.tracks++; + }.bind(this)); + } + this.data.map(function (d, i) { + // Stash a parent reference on the interval + this.data[i].parent = this; + // Determine display range start and end, based on minimum allowable interval display width, + // bounded by what we can see (range: values in terms of pixels on the screen) + this.data[i].display_range = { + start: this.parent.x_scale(Math.max(d[this.layout.start_field], this.state.start)), + end: this.parent.x_scale(Math.min(d[this.layout.end_field], this.state.end)) + }; + this.data[i].display_range.width = this.data[i].display_range.end - this.data[i].display_range.start; + // Convert and stash display range values into domain values + // (domain: values in terms of the data set, e.g. megabases) + this.data[i].display_domain = { + start: this.parent.x_scale.invert(this.data[i].display_range.start), + end: this.parent.x_scale.invert(this.data[i].display_range.end) + }; + this.data[i].display_domain.width = this.data[i].display_domain.end - this.data[i].display_domain.start; + // If splitting to tracks based on the value of the designated track split field + // then don't bother with collision detection (intervals will be grouped on tracks + // solely by the value of track_split_field) + if (this.layout.track_split_field && this.layout.split_tracks) { + var val = this.data[i][this.layout.track_split_field]; + this.data[i].track = this.track_split_field_index[val]; + this.interval_track_index[this.data[i].track].push(i); } else { - potential_track++; - if (potential_track > this.tracks){ - this.tracks = potential_track; - this.interval_track_index[potential_track] = []; + // If not splitting to tracks based on a field value then do so based on collision + // detection (as how it's done for genes). Use display range/domain data generated + // above and cast each interval to tracks such that none overlap + this.tracks = 1; + this.data[i].track = null; + var potential_track = 1; + while (this.data[i].track === null) { + var collision_on_potential_track = false; + this.interval_track_index[potential_track].map(function (placed_interval) { + if (!collision_on_potential_track) { + var min_start = Math.min(placed_interval.display_range.start, this.display_range.start); + var max_end = Math.max(placed_interval.display_range.end, this.display_range.end); + if (max_end - min_start < placed_interval.display_range.width + this.display_range.width) { + collision_on_potential_track = true; + } + } + }.bind(this.data[i])); + if (!collision_on_potential_track) { + this.data[i].track = potential_track; + this.interval_track_index[potential_track].push(this.data[i]); + } else { + potential_track++; + if (potential_track > this.tracks) { + this.tracks = potential_track; + this.interval_track_index[potential_track] = []; + } + } } } - } - - } - - }.bind(this)); - - return this; - }; - - // Implement the main render function - this.render = function(){ - - this.assignTracks(); - - // Remove any shared highlight nodes and re-render them if we're splitting on tracks - // At most there will only be dozen or so nodes here (one per track) and each time - // we render data we may have new tracks, so wiping/redrawing all is reasonable. - this.svg.group.selectAll(".lz-data_layer-intervals-statusnode.lz-data_layer-intervals-shared").remove(); - Object.keys(this.track_split_field_index).forEach(function(key){ - // Make a psuedo-element so that we can generate an id for the shared node - var psuedoElement = {}; - psuedoElement[this.layout.track_split_field] = key; - // Insert the shared node - var sharedstatusnode_style = {display: (this.layout.split_tracks ? null : "none")}; - this.svg.group.insert("rect", ":first-child") - .attr("id", this.getElementStatusNodeId(psuedoElement)) - .attr("class", "lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-shared") - .attr("rx", this.layout.bounding_box_padding).attr("ry", this.layout.bounding_box_padding) - .attr("width", this.parent.layout.cliparea.width) - .attr("height", this.getTrackHeight() - this.layout.track_vertical_spacing) - .attr("x", 0) - .attr("y", (this.track_split_field_index[key]-1) * this.getTrackHeight()) - .style(sharedstatusnode_style); - }.bind(this)); - - var width, height, x, y, fill, fill_opacity; - - // Render interval groups - var selection = this.svg.group.selectAll("g.lz-data_layer-intervals") - .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this)); - - selection.enter().append("g") - .attr("class", "lz-data_layer-intervals"); - - selection.attr("id", function(d){ return this.getElementId(d); }.bind(this)) - .each(function(interval){ - - var data_layer = interval.parent; - - // Render interval status nodes (displayed behind intervals to show highlight - // without needing to modify interval display element(s)) - var statusnode_style = {display: (data_layer.layout.split_tracks ? "none" : null)}; - var statusnodes = d3.select(this).selectAll("rect.lz-data_layer-intervals.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-statusnode-discrete") - .data([interval], function(d){ return data_layer.getElementId(d) + "-statusnode"; }); - statusnodes.enter().insert("rect", ":first-child") - .attr("class", "lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-statusnode-discrete"); - statusnodes - .attr("id", function(d){ - return data_layer.getElementId(d) + "-statusnode"; - }) - .attr("rx", function(){ + }.bind(this)); + return this; + }; + // Implement the main render function + this.render = function () { + this.assignTracks(); + // Remove any shared highlight nodes and re-render them if we're splitting on tracks + // At most there will only be dozen or so nodes here (one per track) and each time + // we render data we may have new tracks, so wiping/redrawing all is reasonable. + this.svg.group.selectAll('.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-shared').remove(); + Object.keys(this.track_split_field_index).forEach(function (key) { + // Make a psuedo-element so that we can generate an id for the shared node + var psuedoElement = {}; + psuedoElement[this.layout.track_split_field] = key; + // Insert the shared node + var sharedstatusnode_style = { display: this.layout.split_tracks ? null : 'none' }; + this.svg.group.insert('rect', ':first-child').attr('id', this.getElementStatusNodeId(psuedoElement)).attr('class', 'lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-shared').attr('rx', this.layout.bounding_box_padding).attr('ry', this.layout.bounding_box_padding).attr('width', this.parent.layout.cliparea.width).attr('height', this.getTrackHeight() - this.layout.track_vertical_spacing).attr('x', 0).attr('y', (this.track_split_field_index[key] - 1) * this.getTrackHeight()).style(sharedstatusnode_style); + }.bind(this)); + var width, height, x, y, fill, fill_opacity; + // Render interval groups + var selection = this.svg.group.selectAll('g.lz-data_layer-intervals').data(this.data, function (d) { + return d[this.layout.id_field]; + }.bind(this)); + selection.enter().append('g').attr('class', 'lz-data_layer-intervals'); + selection.attr('id', function (d) { + return this.getElementId(d); + }.bind(this)).each(function (interval) { + var data_layer = interval.parent; + // Render interval status nodes (displayed behind intervals to show highlight + // without needing to modify interval display element(s)) + var statusnode_style = { display: data_layer.layout.split_tracks ? 'none' : null }; + var statusnodes = d3.select(this).selectAll('rect.lz-data_layer-intervals.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-statusnode-discrete').data([interval], function (d) { + return data_layer.getElementId(d) + '-statusnode'; + }); + statusnodes.enter().insert('rect', ':first-child').attr('class', 'lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-statusnode-discrete'); + statusnodes.attr('id', function (d) { + return data_layer.getElementId(d) + '-statusnode'; + }).attr('rx', function () { return data_layer.layout.bounding_box_padding; - }) - .attr("ry", function(){ + }).attr('ry', function () { return data_layer.layout.bounding_box_padding; - }) - .style(statusnode_style); - width = function(d){ - return d.display_range.width + (2 * data_layer.layout.bounding_box_padding); - }; - height = function(){ - return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; - }; - x = function(d){ - return d.display_range.start - data_layer.layout.bounding_box_padding; - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()); - }; - if (data_layer.canTransition()){ - statusnodes - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } else { - statusnodes - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); - } - statusnodes.exit().remove(); - - // Render primary interval rects - var rects = d3.select(this).selectAll("rect.lz-data_layer-intervals.lz-interval_rect") - .data([interval], function(d){ return d[data_layer.layout.id_field] + "_interval_rect"; }); - - rects.enter().append("rect") - .attr("class", "lz-data_layer-intervals lz-interval_rect"); - - height = data_layer.layout.track_height; - width = function(d){ - return d.display_range.width; - }; - x = function(d){ - return d.display_range.start; - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()) - + data_layer.layout.bounding_box_padding; - }; - fill = function(d){ - return data_layer.resolveScalableParameter(data_layer.layout.color, d); - }; - fill_opacity = function(d){ - return data_layer.resolveScalableParameter(data_layer.layout.fill_opacity, d); - }; - - - if (data_layer.canTransition()){ - rects - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height) - .attr("x", x).attr("y", y) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity); - } else { - rects - .attr("width", width).attr("height", height) - .attr("x", x).attr("y", y) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity); - } - - rects.exit().remove(); - - // Render interval click areas - var clickareas = d3.select(this).selectAll("rect.lz-data_layer-intervals.lz-clickarea") - .data([interval], function(d){ return d.interval_name + "_clickarea"; }); - - clickareas.enter().append("rect") - .attr("class", "lz-data_layer-intervals lz-clickarea"); - - clickareas - .attr("id", function(d){ - return data_layer.getElementId(d) + "_clickarea"; - }) - .attr("rx", function(){ + }).style(statusnode_style); + width = function (d) { + return d.display_range.width + 2 * data_layer.layout.bounding_box_padding; + }; + height = function () { + return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; + }; + x = function (d) { + return d.display_range.start - data_layer.layout.bounding_box_padding; + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight(); + }; + if (data_layer.canTransition()) { + statusnodes.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + statusnodes.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + statusnodes.exit().remove(); + // Render primary interval rects + var rects = d3.select(this).selectAll('rect.lz-data_layer-intervals.lz-interval_rect').data([interval], function (d) { + return d[data_layer.layout.id_field] + '_interval_rect'; + }); + rects.enter().append('rect').attr('class', 'lz-data_layer-intervals lz-interval_rect'); + height = data_layer.layout.track_height; + width = function (d) { + return d.display_range.width; + }; + x = function (d) { + return d.display_range.start; + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight() + data_layer.layout.bounding_box_padding; + }; + fill = function (d) { + return data_layer.resolveScalableParameter(data_layer.layout.color, d); + }; + fill_opacity = function (d) { + return data_layer.resolveScalableParameter(data_layer.layout.fill_opacity, d); + }; + if (data_layer.canTransition()) { + rects.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y).attr('fill', fill).attr('fill-opacity', fill_opacity); + } else { + rects.attr('width', width).attr('height', height).attr('x', x).attr('y', y).attr('fill', fill).attr('fill-opacity', fill_opacity); + } + rects.exit().remove(); + // Render interval click areas + var clickareas = d3.select(this).selectAll('rect.lz-data_layer-intervals.lz-clickarea').data([interval], function (d) { + return d.interval_name + '_clickarea'; + }); + clickareas.enter().append('rect').attr('class', 'lz-data_layer-intervals lz-clickarea'); + clickareas.attr('id', function (d) { + return data_layer.getElementId(d) + '_clickarea'; + }).attr('rx', function () { return data_layer.layout.bounding_box_padding; - }) - .attr("ry", function(){ + }).attr('ry', function () { return data_layer.layout.bounding_box_padding; }); - - width = function(d){ - return d.display_range.width; - }; - height = function(){ - return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; - }; - x = function(d){ - return d.display_range.start; - }; - y = function(d){ - return ((d.track-1) * data_layer.getTrackHeight()); - }; - if (data_layer.canTransition()){ - clickareas - .transition() - .duration(data_layer.layout.transition.duration || 0) - .ease(data_layer.layout.transition.ease || "cubic-in-out") - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); + width = function (d) { + return d.display_range.width; + }; + height = function () { + return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing; + }; + x = function (d) { + return d.display_range.start; + }; + y = function (d) { + return (d.track - 1) * data_layer.getTrackHeight(); + }; + if (data_layer.canTransition()) { + clickareas.transition().duration(data_layer.layout.transition.duration || 0).ease(data_layer.layout.transition.ease || 'cubic-in-out').attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } else { + clickareas.attr('width', width).attr('height', height).attr('x', x).attr('y', y); + } + // Remove old clickareas as needed + clickareas.exit().remove(); + // Apply default event emitters to clickareas + clickareas.on('click', function (element) { + element.parent.parent.emit('element_clicked', element); + element.parent.parent_plot.emit('element_clicked', element); + }.bind(this)); + // Apply mouse behaviors to clickareas + data_layer.applyBehaviors(clickareas); + }); + // Remove old elements as needed + selection.exit().remove(); + // Update the legend axis if the number of ticks changed + if (this.previous_tracks !== this.tracks) { + this.updateSplitTrackAxis(); + } + return this; + }; + // Reimplement the positionTooltip() method to be interval-specific + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var tooltip = this.tooltips[id]; + var arrow_width = 7; + // as defined in the default stylesheet + var stroke_width = 1; + // as defined in the default stylesheet + var page_origin = this.getPageOrigin(); + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + var interval_bbox = d3.select('#' + this.getElementStatusNodeId(tooltip.data)).node().getBBox(); + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); + // Position horizontally: attempt to center on the portion of the interval that's visible, + // pad to either side if bumping up against the edge of the data layer + var interval_center_x = (tooltip.data.display_range.start + tooltip.data.display_range.end) / 2 - this.layout.bounding_box_padding / 2; + var offset_right = Math.max(tooltip_box.width / 2 - interval_center_x, 0); + var offset_left = Math.max(tooltip_box.width / 2 + interval_center_x - data_layer_width, 0); + var left = page_origin.x + interval_center_x - tooltip_box.width / 2 - offset_left + offset_right; + var arrow_left = tooltip_box.width / 2 - arrow_width / 2 + offset_left - offset_right; + // Position vertically below the interval unless there's insufficient space + var top, arrow_type, arrow_top; + if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (interval_bbox.y + interval_bbox.height)) { + top = page_origin.y + interval_bbox.y - (tooltip_box.height + stroke_width + arrow_width); + arrow_type = 'down'; + arrow_top = tooltip_box.height - stroke_width; } else { - clickareas - .attr("width", width).attr("height", height).attr("x", x).attr("y", y); + top = page_origin.y + interval_bbox.y + interval_bbox.height + stroke_width + arrow_width; + arrow_type = 'up'; + arrow_top = 0 - stroke_width - arrow_width; } - - // Remove old clickareas as needed - clickareas.exit().remove(); - - // Apply default event emitters to clickareas - clickareas.on("click", function(element){ - element.parent.parent.emit("element_clicked", element); - element.parent.parent_plot.emit("element_clicked", element); - }.bind(this)); - - // Apply mouse behaviors to clickareas - data_layer.applyBehaviors(clickareas); - - }); - - // Remove old elements as needed - selection.exit().remove(); - - // Update the legend axis if the number of ticks changed - if (this.previous_tracks !== this.tracks){ - this.updateSplitTrackAxis(); - } - - return this; - - }; - - // Reimplement the positionTooltip() method to be interval-specific - this.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]){ - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var tooltip = this.tooltips[id]; - var arrow_width = 7; // as defined in the default stylesheet - var stroke_width = 1; // as defined in the default stylesheet - var page_origin = this.getPageOrigin(); - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - var interval_bbox = d3.select("#" + this.getElementStatusNodeId(tooltip.data)).node().getBBox(); - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); - // Position horizontally: attempt to center on the portion of the interval that's visible, - // pad to either side if bumping up against the edge of the data layer - var interval_center_x = ((tooltip.data.display_range.start + tooltip.data.display_range.end) / 2) - (this.layout.bounding_box_padding / 2); - var offset_right = Math.max((tooltip_box.width / 2) - interval_center_x, 0); - var offset_left = Math.max((tooltip_box.width / 2) + interval_center_x - data_layer_width, 0); - var left = page_origin.x + interval_center_x - (tooltip_box.width / 2) - offset_left + offset_right; - var arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right; - // Position vertically below the interval unless there's insufficient space - var top, arrow_type, arrow_top; - if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (interval_bbox.y + interval_bbox.height)){ - top = page_origin.y + interval_bbox.y - (tooltip_box.height + stroke_width + arrow_width); - arrow_type = "down"; - arrow_top = tooltip_box.height - stroke_width; - } else { - top = page_origin.y + interval_bbox.y + interval_bbox.height + stroke_width + arrow_width; - arrow_type = "up"; - arrow_top = 0 - stroke_width - arrow_width; - } - // Apply positions to the main div - tooltip.selector.style("left", left + "px").style("top", top + "px"); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow){ - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style("left", arrow_left + "px") - .style("top", arrow_top + "px"); - }; - - // Redraw split track axis or hide it, and show/hide the legend, as determined - // by current layout parameters and data - this.updateSplitTrackAxis = function(){ - var legend_axis = this.layout.track_split_legend_to_y_axis ? "y" + this.layout.track_split_legend_to_y_axis : false; - if (this.layout.split_tracks){ - var tracks = +this.tracks || 0; - var track_height = +this.layout.track_height || 0; - var track_spacing = 2 * (+this.layout.bounding_box_padding || 0) + (+this.layout.track_vertical_spacing || 0); - var target_height = (tracks * track_height) + ((tracks - 1) * track_spacing); - this.parent.scaleHeightToData(target_height); - if (legend_axis && this.parent.legend){ - this.parent.legend.hide(); - this.parent.layout.axes[legend_axis] = { - render: true, - ticks: [], - range: { - start: (target_height - (this.layout.track_height/2)), - end: (this.layout.track_height/2) + // Apply positions to the main div + tooltip.selector.style('left', left + 'px').style('top', top + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style('left', arrow_left + 'px').style('top', arrow_top + 'px'); + }; + // Redraw split track axis or hide it, and show/hide the legend, as determined + // by current layout parameters and data + this.updateSplitTrackAxis = function () { + var legend_axis = this.layout.track_split_legend_to_y_axis ? 'y' + this.layout.track_split_legend_to_y_axis : false; + if (this.layout.split_tracks) { + var tracks = +this.tracks || 0; + var track_height = +this.layout.track_height || 0; + var track_spacing = 2 * (+this.layout.bounding_box_padding || 0) + (+this.layout.track_vertical_spacing || 0); + var target_height = tracks * track_height + (tracks - 1) * track_spacing; + this.parent.scaleHeightToData(target_height); + if (legend_axis && this.parent.legend) { + this.parent.legend.hide(); + this.parent.layout.axes[legend_axis] = { + render: true, + ticks: [], + range: { + start: target_height - this.layout.track_height / 2, + end: this.layout.track_height / 2 + } + }; + this.layout.legend.forEach(function (element) { + var key = element[this.layout.track_split_field]; + var track = this.track_split_field_index[key]; + if (track) { + if (this.layout.track_split_order === 'DESC') { + track = Math.abs(track - tracks - 1); + } + this.parent.layout.axes[legend_axis].ticks.push({ + y: track, + text: element.label + }); + } + }.bind(this)); + this.layout.y_axis = { + axis: this.layout.track_split_legend_to_y_axis, + floor: 1, + ceiling: tracks + }; + this.parent.render(); } - }; - this.layout.legend.forEach(function(element){ - var key = element[this.layout.track_split_field]; - var track = this.track_split_field_index[key]; - if (track){ - if (this.layout.track_split_order === "DESC"){ - track = Math.abs(track - tracks - 1); + this.parent_plot.positionPanels(); + } else { + if (legend_axis && this.parent.legend) { + if (!this.layout.always_hide_legend) { + this.parent.legend.show(); } - this.parent.layout.axes[legend_axis].ticks.push({ - y: track, - text: element.label - }); + this.parent.layout.axes[legend_axis] = { render: false }; + this.parent.render(); } - }.bind(this)); - this.layout.y_axis = { - axis: this.layout.track_split_legend_to_y_axis, - floor: 1, - ceiling: tracks - }; - this.parent.render(); - } - this.parent_plot.positionPanels(); - } else { - if (legend_axis && this.parent.legend){ - if (!this.layout.always_hide_legend){ this.parent.legend.show(); } - this.parent.layout.axes[legend_axis] = { render: false }; - this.parent.render(); - } - } - return this; - }; - - // Method to not only toggle the split tracks boolean but also update - // necessary display values to animate a complete merge/split - this.toggleSplitTracks = function(){ - this.layout.split_tracks = !this.layout.split_tracks; - if (this.parent.legend && !this.layout.always_hide_legend){ - this.parent.layout.margin.bottom = 5 + (this.layout.split_tracks ? 0 : this.parent.legend.layout.height + 5); - } - this.render(); - this.updateSplitTrackAxis(); - return this; - }; - - return this; - -}); - -"use strict"; - -/********************* + } + return this; + }; + // Method to not only toggle the split tracks boolean but also update + // necessary display values to animate a complete merge/split + this.toggleSplitTracks = function () { + this.layout.split_tracks = !this.layout.split_tracks; + if (this.parent.legend && !this.layout.always_hide_legend) { + this.parent.layout.margin.bottom = 5 + (this.layout.split_tracks ? 0 : this.parent.legend.layout.height + 5); + } + this.render(); + this.updateSplitTrackAxis(); + return this; + }; + return this; + }); + 'use strict'; + /********************* * Line Data Layer * Implements a standard line plot * @class * @augments LocusZoom.DataLayer */ -LocusZoom.DataLayers.add("line", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - /** @member {Object} */ - this.DefaultLayout = { - style: { - fill: "none", - "stroke-width": "2px" - }, - interpolate: "linear", - x_axis: { field: "x" }, - y_axis: { field: "y", axis: 1 }, - hitarea_width: 5 - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Var for storing mouse events for use in tool tip positioning - /** @member {String} */ - this.mouse_event = null; - - /** + LocusZoom.DataLayers.add('line', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + /** @member {Object} */ + this.DefaultLayout = { + style: { + fill: 'none', + 'stroke-width': '2px' + }, + interpolate: 'linear', + x_axis: { field: 'x' }, + y_axis: { + field: 'y', + axis: 1 + }, + hitarea_width: 5 + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Var for storing mouse events for use in tool tip positioning + /** @member {String} */ + this.mouse_event = null; + /** * Var for storing the generated line function itself * @member {d3.svg.line} * */ - this.line = null; - - /** + this.line = null; + /** * The timeout identifier returned by setTimeout * @member {Number} */ - this.tooltip_timeout = null; - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - - /** + this.tooltip_timeout = null; + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + /** * Helper function to get display and data objects representing * the x/y coordinates of the current mouse event with respect to the line in terms of the display * and the interpolated values of the x/y fields with respect to the line * @returns {{display: {x: *, y: null}, data: {}, slope: null}} */ - this.getMouseDisplayAndData = function(){ - var ret = { - display: { - x: d3.mouse(this.mouse_event)[0], - y: null - }, - data: {}, - slope: null - }; - var x_field = this.layout.x_axis.field; - var y_field = this.layout.y_axis.field; - var x_scale = "x_scale"; - var y_scale = "y" + this.layout.y_axis.axis + "_scale"; - ret.data[x_field] = this.parent[x_scale].invert(ret.display.x); - var bisect = d3.bisector(function(datum) { return +datum[x_field]; }).left; - var index = bisect(this.data, ret.data[x_field]) - 1; - var startDatum = this.data[index]; - var endDatum = this.data[index + 1]; - var interpolate = d3.interpolateNumber(+startDatum[y_field], +endDatum[y_field]); - var range = +endDatum[x_field] - +startDatum[x_field]; - ret.data[y_field] = interpolate((ret.data[x_field] % range) / range); - ret.display.y = this.parent[y_scale](ret.data[y_field]); - if (this.layout.tooltip.x_precision){ - ret.data[x_field] = ret.data[x_field].toPrecision(this.layout.tooltip.x_precision); - } - if (this.layout.tooltip.y_precision){ - ret.data[y_field] = ret.data[y_field].toPrecision(this.layout.tooltip.y_precision); - } - ret.slope = (this.parent[y_scale](endDatum[y_field]) - this.parent[y_scale](startDatum[y_field])) - / (this.parent[x_scale](endDatum[x_field]) - this.parent[x_scale](startDatum[x_field])); - return ret; - }; - - /** + this.getMouseDisplayAndData = function () { + var ret = { + display: { + x: d3.mouse(this.mouse_event)[0], + y: null + }, + data: {}, + slope: null + }; + var x_field = this.layout.x_axis.field; + var y_field = this.layout.y_axis.field; + var x_scale = 'x_scale'; + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + ret.data[x_field] = this.parent[x_scale].invert(ret.display.x); + var bisect = d3.bisector(function (datum) { + return +datum[x_field]; + }).left; + var index = bisect(this.data, ret.data[x_field]) - 1; + var startDatum = this.data[index]; + var endDatum = this.data[index + 1]; + var interpolate = d3.interpolateNumber(+startDatum[y_field], +endDatum[y_field]); + var range = +endDatum[x_field] - +startDatum[x_field]; + ret.data[y_field] = interpolate(ret.data[x_field] % range / range); + ret.display.y = this.parent[y_scale](ret.data[y_field]); + if (this.layout.tooltip.x_precision) { + ret.data[x_field] = ret.data[x_field].toPrecision(this.layout.tooltip.x_precision); + } + if (this.layout.tooltip.y_precision) { + ret.data[y_field] = ret.data[y_field].toPrecision(this.layout.tooltip.y_precision); + } + ret.slope = (this.parent[y_scale](endDatum[y_field]) - this.parent[y_scale](startDatum[y_field])) / (this.parent[x_scale](endDatum[x_field]) - this.parent[x_scale](startDatum[x_field])); + return ret; + }; + /** * Reimplement the positionTooltip() method to be line-specific * @param {String} id Identify the tooltip to be positioned */ - this.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]){ - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var tooltip = this.tooltips[id]; - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - var arrow_width = 7; // as defined in the default stylesheet - var border_radius = 6; // as defined in the default stylesheet - var stroke_width = parseFloat(this.layout.style["stroke-width"]) || 1; - var page_origin = this.getPageOrigin(); - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); - var top, left, arrow_top, arrow_left, arrow_type; - - // Determine x/y coordinates for display and data - var dd = this.getMouseDisplayAndData(); - - // If the absolute value of the slope of the line at this point is above 1 (including Infinity) - // then position the tool tip left/right. Otherwise position top/bottom. - if (Math.abs(dd.slope) > 1){ - - // Position horizontally on the left or the right depending on which side of the plot the point is on - if (dd.display.x <= this.parent.layout.width / 2){ - left = page_origin.x + dd.display.x + stroke_width + arrow_width + stroke_width; - arrow_type = "left"; - arrow_left = -1 * (arrow_width + stroke_width); - } else { - left = page_origin.x + dd.display.x - tooltip_box.width - stroke_width - arrow_width - stroke_width; - arrow_type = "right"; - arrow_left = tooltip_box.width - stroke_width; - } - // Position vertically centered unless we're at the top or bottom of the plot - if (dd.display.y - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down - top = page_origin.y + dd.display.y - (1.5 * arrow_width) - border_radius; - arrow_top = border_radius; - } else if (dd.display.y + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up - top = page_origin.y + dd.display.y + arrow_width + border_radius - tooltip_box.height; - arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius; - } else { // vertically centered - top = page_origin.y + dd.display.y - (tooltip_box.height / 2); - arrow_top = (tooltip_box.height / 2) - arrow_width; - } - - } else { - - // Position horizontally: attempt to center on the mouse's x coordinate - // pad to either side if bumping up against the edge of the data layer - var offset_right = Math.max((tooltip_box.width / 2) - dd.display.x, 0); - var offset_left = Math.max((tooltip_box.width / 2) + dd.display.x - data_layer_width, 0); - left = page_origin.x + dd.display.x - (tooltip_box.width / 2) - offset_left + offset_right; - var min_arrow_left = arrow_width / 2; - var max_arrow_left = tooltip_box.width - (2.5 * arrow_width); - arrow_left = (tooltip_box.width / 2) - arrow_width + offset_left - offset_right; - arrow_left = Math.min(Math.max(arrow_left, min_arrow_left), max_arrow_left); - - // Position vertically above the line unless there's insufficient space - if (tooltip_box.height + stroke_width + arrow_width > dd.display.y){ - top = page_origin.y + dd.display.y + stroke_width + arrow_width; - arrow_type = "up"; - arrow_top = 0 - stroke_width - arrow_width; - } else { - top = page_origin.y + dd.display.y - (tooltip_box.height + stroke_width + arrow_width); - arrow_type = "down"; - arrow_top = tooltip_box.height - stroke_width; - } - } - - // Apply positions to the main div - tooltip.selector.style({ left: left + "px", top: top + "px" }); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow){ - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style({ "left": arrow_left + "px", top: arrow_top + "px" }); - - }; - - /** + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var tooltip = this.tooltips[id]; + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + var arrow_width = 7; + // as defined in the default stylesheet + var border_radius = 6; + // as defined in the default stylesheet + var stroke_width = parseFloat(this.layout.style['stroke-width']) || 1; + var page_origin = this.getPageOrigin(); + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); + var top, left, arrow_top, arrow_left, arrow_type; + // Determine x/y coordinates for display and data + var dd = this.getMouseDisplayAndData(); + // If the absolute value of the slope of the line at this point is above 1 (including Infinity) + // then position the tool tip left/right. Otherwise position top/bottom. + if (Math.abs(dd.slope) > 1) { + // Position horizontally on the left or the right depending on which side of the plot the point is on + if (dd.display.x <= this.parent.layout.width / 2) { + left = page_origin.x + dd.display.x + stroke_width + arrow_width + stroke_width; + arrow_type = 'left'; + arrow_left = -1 * (arrow_width + stroke_width); + } else { + left = page_origin.x + dd.display.x - tooltip_box.width - stroke_width - arrow_width - stroke_width; + arrow_type = 'right'; + arrow_left = tooltip_box.width - stroke_width; + } + // Position vertically centered unless we're at the top or bottom of the plot + if (dd.display.y - tooltip_box.height / 2 <= 0) { + // Too close to the top, push it down + top = page_origin.y + dd.display.y - 1.5 * arrow_width - border_radius; + arrow_top = border_radius; + } else if (dd.display.y + tooltip_box.height / 2 >= data_layer_height) { + // Too close to the bottom, pull it up + top = page_origin.y + dd.display.y + arrow_width + border_radius - tooltip_box.height; + arrow_top = tooltip_box.height - 2 * arrow_width - border_radius; + } else { + // vertically centered + top = page_origin.y + dd.display.y - tooltip_box.height / 2; + arrow_top = tooltip_box.height / 2 - arrow_width; + } + } else { + // Position horizontally: attempt to center on the mouse's x coordinate + // pad to either side if bumping up against the edge of the data layer + var offset_right = Math.max(tooltip_box.width / 2 - dd.display.x, 0); + var offset_left = Math.max(tooltip_box.width / 2 + dd.display.x - data_layer_width, 0); + left = page_origin.x + dd.display.x - tooltip_box.width / 2 - offset_left + offset_right; + var min_arrow_left = arrow_width / 2; + var max_arrow_left = tooltip_box.width - 2.5 * arrow_width; + arrow_left = tooltip_box.width / 2 - arrow_width + offset_left - offset_right; + arrow_left = Math.min(Math.max(arrow_left, min_arrow_left), max_arrow_left); + // Position vertically above the line unless there's insufficient space + if (tooltip_box.height + stroke_width + arrow_width > dd.display.y) { + top = page_origin.y + dd.display.y + stroke_width + arrow_width; + arrow_type = 'up'; + arrow_top = 0 - stroke_width - arrow_width; + } else { + top = page_origin.y + dd.display.y - (tooltip_box.height + stroke_width + arrow_width); + arrow_type = 'down'; + arrow_top = tooltip_box.height - stroke_width; + } + } + // Apply positions to the main div + tooltip.selector.style({ + left: left + 'px', + top: top + 'px' + }); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style({ + 'left': arrow_left + 'px', + top: arrow_top + 'px' + }); + }; + /** * Implement the main render function */ - this.render = function(){ - - // Several vars needed to be in scope - var data_layer = this; - var panel = this.parent; - var x_field = this.layout.x_axis.field; - var y_field = this.layout.y_axis.field; - var x_scale = "x_scale"; - var y_scale = "y" + this.layout.y_axis.axis + "_scale"; - - // Join data to the line selection - var selection = this.svg.group - .selectAll("path.lz-data_layer-line") - .data([this.data]); - - // Create path element, apply class - this.path = selection.enter() - .append("path") - .attr("class", "lz-data_layer-line"); - - // Generate the line - this.line = d3.svg.line() - .x(function(d) { return parseFloat(panel[x_scale](d[x_field])); }) - .y(function(d) { return parseFloat(panel[y_scale](d[y_field])); }) - .interpolate(this.layout.interpolate); - - // Apply line and style - if (this.canTransition()){ - selection - .transition() - .duration(this.layout.transition.duration || 0) - .ease(this.layout.transition.ease || "cubic-in-out") - .attr("d", this.line) - .style(this.layout.style); - } else { - selection - .attr("d", this.line) - .style(this.layout.style); - } - - // Apply tooltip, etc - if (this.layout.tooltip){ - // Generate an overlaying transparent "hit area" line for more intuitive mouse events - var hitarea_width = parseFloat(this.layout.hitarea_width).toString() + "px"; - var hitarea = this.svg.group - .selectAll("path.lz-data_layer-line-hitarea") - .data([this.data]); - hitarea.enter() - .append("path") - .attr("class", "lz-data_layer-line-hitarea") - .style("stroke-width", hitarea_width); - var hitarea_line = d3.svg.line() - .x(function(d) { return parseFloat(panel[x_scale](d[x_field])); }) - .y(function(d) { return parseFloat(panel[y_scale](d[y_field])); }) - .interpolate(this.layout.interpolate); - hitarea - .attr("d", hitarea_line) - .on("mouseover", function(){ - clearTimeout(data_layer.tooltip_timeout); - data_layer.mouse_event = this; - var dd = data_layer.getMouseDisplayAndData(); - data_layer.createTooltip(dd.data); - }) - .on("mousemove", function(){ - clearTimeout(data_layer.tooltip_timeout); - data_layer.mouse_event = this; - var dd = data_layer.getMouseDisplayAndData(); - data_layer.updateTooltip(dd.data); - data_layer.positionTooltip(data_layer.getElementId()); - }) - .on("mouseout", function(){ - data_layer.tooltip_timeout = setTimeout(function(){ - data_layer.mouse_event = null; - data_layer.destroyTooltip(data_layer.getElementId()); - }, 300); - }); - hitarea.exit().remove(); - } - - // Remove old elements as needed - selection.exit().remove(); - - }; - - /** + this.render = function () { + // Several vars needed to be in scope + var data_layer = this; + var panel = this.parent; + var x_field = this.layout.x_axis.field; + var y_field = this.layout.y_axis.field; + var x_scale = 'x_scale'; + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + // Join data to the line selection + var selection = this.svg.group.selectAll('path.lz-data_layer-line').data([this.data]); + // Create path element, apply class + this.path = selection.enter().append('path').attr('class', 'lz-data_layer-line'); + // Generate the line + this.line = d3.svg.line().x(function (d) { + return parseFloat(panel[x_scale](d[x_field])); + }).y(function (d) { + return parseFloat(panel[y_scale](d[y_field])); + }).interpolate(this.layout.interpolate); + // Apply line and style + if (this.canTransition()) { + selection.transition().duration(this.layout.transition.duration || 0).ease(this.layout.transition.ease || 'cubic-in-out').attr('d', this.line).style(this.layout.style); + } else { + selection.attr('d', this.line).style(this.layout.style); + } + // Apply tooltip, etc + if (this.layout.tooltip) { + // Generate an overlaying transparent "hit area" line for more intuitive mouse events + var hitarea_width = parseFloat(this.layout.hitarea_width).toString() + 'px'; + var hitarea = this.svg.group.selectAll('path.lz-data_layer-line-hitarea').data([this.data]); + hitarea.enter().append('path').attr('class', 'lz-data_layer-line-hitarea').style('stroke-width', hitarea_width); + var hitarea_line = d3.svg.line().x(function (d) { + return parseFloat(panel[x_scale](d[x_field])); + }).y(function (d) { + return parseFloat(panel[y_scale](d[y_field])); + }).interpolate(this.layout.interpolate); + hitarea.attr('d', hitarea_line).on('mouseover', function () { + clearTimeout(data_layer.tooltip_timeout); + data_layer.mouse_event = this; + var dd = data_layer.getMouseDisplayAndData(); + data_layer.createTooltip(dd.data); + }).on('mousemove', function () { + clearTimeout(data_layer.tooltip_timeout); + data_layer.mouse_event = this; + var dd = data_layer.getMouseDisplayAndData(); + data_layer.updateTooltip(dd.data); + data_layer.positionTooltip(data_layer.getElementId()); + }).on('mouseout', function () { + data_layer.tooltip_timeout = setTimeout(function () { + data_layer.mouse_event = null; + data_layer.destroyTooltip(data_layer.getElementId()); + }, 300); + }); + hitarea.exit().remove(); + } + // Remove old elements as needed + selection.exit().remove(); + }; + /** * Redefine setElementStatus family of methods as line data layers will only ever have a single path element * @param {String} status A member of `LocusZoom.DataLayer.Statuses.adjectives` * @param {String|Object} element * @param {Boolean} toggle * @returns {LocusZoom.DataLayer} */ - this.setElementStatus = function(status, element, toggle){ - return this.setAllElementStatus(status, toggle); - }; - this.setElementStatusByFilters = function(status, toggle){ - return this.setAllElementStatus(status, toggle); - }; - this.setAllElementStatus = function(status, toggle){ - // Sanity check - if (typeof status == "undefined" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){ - throw("Invalid status passed to DataLayer.setAllElementStatus()"); - } - if (typeof this.state[this.state_id][status] == "undefined"){ return this; } - if (typeof toggle == "undefined"){ toggle = true; } - - // Update global status flag - this.global_statuses[status] = toggle; - - // Apply class to path based on global status flags - var path_class = "lz-data_layer-line"; - Object.keys(this.global_statuses).forEach(function(global_status){ - if (this.global_statuses[global_status]){ path_class += " lz-data_layer-line-" + global_status; } - }.bind(this)); - this.path.attr("class", path_class); - - // Trigger layout changed event hook - this.parent.emit("layout_changed"); - this.parent_plot.emit("layout_changed"); - - return this; - }; - - return this; - -}); - - -/*************************** + this.setElementStatus = function (status, element, toggle) { + return this.setAllElementStatus(status, toggle); + }; + this.setElementStatusByFilters = function (status, toggle) { + return this.setAllElementStatus(status, toggle); + }; + this.setAllElementStatus = function (status, toggle) { + // Sanity check + if (typeof status == 'undefined' || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1) { + throw 'Invalid status passed to DataLayer.setAllElementStatus()'; + } + if (typeof this.state[this.state_id][status] == 'undefined') { + return this; + } + if (typeof toggle == 'undefined') { + toggle = true; + } + // Update global status flag + this.global_statuses[status] = toggle; + // Apply class to path based on global status flags + var path_class = 'lz-data_layer-line'; + Object.keys(this.global_statuses).forEach(function (global_status) { + if (this.global_statuses[global_status]) { + path_class += ' lz-data_layer-line-' + global_status; + } + }.bind(this)); + this.path.attr('class', path_class); + // Trigger layout changed event hook + this.parent.emit('layout_changed'); + this.parent_plot.emit('layout_changed'); + return this; + }; + return this; + }); + /*************************** * Orthogonal Line Data Layer * Implements a horizontal or vertical line given an orientation and an offset in the layout * Does not require a data source * @class * @augments LocusZoom.DataLayer */ -LocusZoom.DataLayers.add("orthogonal_line", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - this.DefaultLayout = { - style: { - "stroke": "#D3D3D3", - "stroke-width": "3px", - "stroke-dasharray": "10px 10px" - }, - orientation: "horizontal", - x_axis: { - axis: 1, - decoupled: true - }, - y_axis: { - axis: 1, - decoupled: true - }, - offset: 0 - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Require that orientation be "horizontal" or "vertical" only - if (["horizontal","vertical"].indexOf(layout.orientation) === -1){ - layout.orientation = "horizontal"; - } - - // Vars for storing the data generated line - /** @member {Array} */ - this.data = []; - /** @member {d3.svg.line} */ - this.line = null; - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - /** - * Implement the main render function - */ - this.render = function(){ - - // Several vars needed to be in scope - var panel = this.parent; - var x_scale = "x_scale"; - var y_scale = "y" + this.layout.y_axis.axis + "_scale"; - var x_extent = "x_extent"; - var y_extent = "y" + this.layout.y_axis.axis + "_extent"; - var x_range = "x_range"; - var y_range = "y" + this.layout.y_axis.axis + "_range"; - - // Generate data using extents depending on orientation - if (this.layout.orientation === "horizontal"){ - this.data = [ - { x: panel[x_extent][0], y: this.layout.offset }, - { x: panel[x_extent][1], y: this.layout.offset } - ]; - } else { - this.data = [ - { x: this.layout.offset, y: panel[y_extent][0] }, - { x: this.layout.offset, y: panel[y_extent][1] } - ]; - } - - // Join data to the line selection - var selection = this.svg.group - .selectAll("path.lz-data_layer-line") - .data([this.data]); - - // Create path element, apply class - this.path = selection.enter() - .append("path") - .attr("class", "lz-data_layer-line"); - - // Generate the line - this.line = d3.svg.line() - .x(function(d, i) { - var x = parseFloat(panel[x_scale](d["x"])); - return isNaN(x) ? panel[x_range][i] : x; - }) - .y(function(d, i) { - var y = parseFloat(panel[y_scale](d["y"])); - return isNaN(y) ? panel[y_range][i] : y; - }) - .interpolate("linear"); - - // Apply line and style - if (this.canTransition()){ - selection - .transition() - .duration(this.layout.transition.duration || 0) - .ease(this.layout.transition.ease || "cubic-in-out") - .attr("d", this.line) - .style(this.layout.style); - } else { - selection - .attr("d", this.line) - .style(this.layout.style); - } - - // Remove old elements as needed - selection.exit().remove(); - - }; - - return this; - -}); - -"use strict"; - -/********************* - Scatter Data Layer - Implements a standard scatter plot -*/ - -LocusZoom.DataLayers.add("scatter", function(layout){ - - // Define a default layout for this DataLayer type and merge it with the passed argument - this.DefaultLayout = { - point_size: 40, - point_shape: "circle", - tooltip_positioning: "horizontal", - color: "#888888", - fill_opacity: 1, - y_axis: { - axis: 1 - }, - id_field: "id" - }; - layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); - - // Extra default for layout spacing - // Not in default layout since that would make the label attribute always present - if (layout.label && isNaN(layout.label.spacing)){ - layout.label.spacing = 4; - } - - // Apply the arguments to set LocusZoom.DataLayer as the prototype - LocusZoom.DataLayer.apply(this, arguments); - - // Reimplement the positionTooltip() method to be scatter-specific - this.positionTooltip = function(id){ - if (typeof id != "string"){ - throw ("Unable to position tooltip: id is not a string"); - } - if (!this.tooltips[id]){ - throw ("Unable to position tooltip: id does not point to a valid tooltip"); - } - var top, left, arrow_type, arrow_top, arrow_left; - var tooltip = this.tooltips[id]; - var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data); - var offset = Math.sqrt(point_size / Math.PI); - var arrow_width = 7; // as defined in the default stylesheet - var stroke_width = 1; // as defined in the default stylesheet - var border_radius = 6; // as defined in the default stylesheet - var page_origin = this.getPageOrigin(); - var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); - var y_scale = "y"+this.layout.y_axis.axis+"_scale"; - var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]); - var tooltip_box = tooltip.selector.node().getBoundingClientRect(); - var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); - if (this.layout.tooltip_positioning === "vertical"){ - // Position horizontally centered above the point - var offset_right = Math.max((tooltip_box.width / 2) - x_center, 0); - var offset_left = Math.max((tooltip_box.width / 2) + x_center - data_layer_width, 0); - left = page_origin.x + x_center - (tooltip_box.width / 2) - offset_left + offset_right; - arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right - offset; - // Position vertically above the point unless there's insufficient space, then go below - if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (y_center + offset)){ - top = page_origin.y + y_center - (offset + tooltip_box.height + stroke_width + arrow_width); - arrow_type = "down"; - arrow_top = tooltip_box.height - stroke_width; - } else { - top = page_origin.y + y_center + offset + stroke_width + arrow_width; - arrow_type = "up"; - arrow_top = 0 - stroke_width - arrow_width; - } - } else { - // Position horizontally on the left or the right depending on which side of the plot the point is on - if (x_center <= this.parent.layout.width / 2){ - left = page_origin.x + x_center + offset + arrow_width + stroke_width; - arrow_type = "left"; - arrow_left = -1 * (arrow_width + stroke_width); - } else { - left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width; - arrow_type = "right"; - arrow_left = tooltip_box.width - stroke_width; - } - // Position vertically centered unless we're at the top or bottom of the plot - data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); - if (y_center - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down - top = page_origin.y + y_center - (1.5 * arrow_width) - border_radius; - arrow_top = border_radius; - } else if (y_center + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up - top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height; - arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius; - } else { // vertically centered - top = page_origin.y + y_center - (tooltip_box.height / 2); - arrow_top = (tooltip_box.height / 2) - arrow_width; - } - } - // Apply positions to the main div - tooltip.selector.style("left", left + "px").style("top", top + "px"); - // Create / update position on arrow connecting tooltip to data - if (!tooltip.arrow){ - tooltip.arrow = tooltip.selector.append("div").style("position", "absolute"); - } - tooltip.arrow - .attr("class", "lz-data_layer-tooltip-arrow_" + arrow_type) - .style("left", arrow_left + "px") - .style("top", arrow_top + "px"); - }; - - // Function to flip labels from being anchored at the start of the text to the end - // Both to keep labels from running outside the data layer and also as a first - // pass on recursive separation - this.flip_labels = function(){ - var data_layer = this; - var point_size = data_layer.resolveScalableParameter(data_layer.layout.point_size, {}); - var spacing = data_layer.layout.label.spacing; - var handle_lines = Boolean(data_layer.layout.label.lines); - var min_x = 2 * spacing; - var max_x = data_layer.parent.layout.width - data_layer.parent.layout.margin.left - data_layer.parent.layout.margin.right - (2 * spacing); - var flip = function(dn, dnl){ - var dnx = +dn.attr("x"); - var text_swing = (2 * spacing) + (2 * Math.sqrt(point_size)); - if (handle_lines){ - var dnlx2 = +dnl.attr("x2"); - var line_swing = spacing + (2 * Math.sqrt(point_size)); - } - if (dn.style("text-anchor") === "start"){ - dn.style("text-anchor", "end"); - dn.attr("x", dnx - text_swing); - if (handle_lines){ dnl.attr("x2", dnlx2 - line_swing); } - } else { - dn.style("text-anchor", "start"); - dn.attr("x", dnx + text_swing); - if (handle_lines){ dnl.attr("x2", dnlx2 + line_swing); } - } - }; - // Flip any going over the right edge from the right side to the left side - // (all labels start on the right side) - data_layer.label_texts.each(function (d, i) { - var a = this; - var da = d3.select(a); - var dax = +da.attr("x"); - var abound = da.node().getBoundingClientRect(); - if (dax + abound.width + spacing > max_x){ - var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null; - flip(da, dal); - } - }); - // Second pass to flip any others that haven't flipped yet if they collide with another label - data_layer.label_texts.each(function (d, i) { - var a = this; - var da = d3.select(a); - if (da.style("text-anchor") === "end") return; - var dax = +da.attr("x"); - var abound = da.node().getBoundingClientRect(); - var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null; - data_layer.label_texts.each(function () { - var b = this; - var db = d3.select(b); - var bbound = db.node().getBoundingClientRect(); - var collision = abound.left < bbound.left + bbound.width + (2*spacing) && - abound.left + abound.width + (2*spacing) > bbound.left && - abound.top < bbound.top + bbound.height + (2*spacing) && - abound.height + abound.top + (2*spacing) > bbound.top; - if (collision){ - flip(da, dal); - // Double check that this flip didn't push the label past min_x. If it did, immediately flip back. - dax = +da.attr("x"); - if (dax - abound.width - spacing < min_x){ - flip(da, dal); - } + LocusZoom.DataLayers.add('orthogonal_line', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + this.DefaultLayout = { + style: { + 'stroke': '#D3D3D3', + 'stroke-width': '3px', + 'stroke-dasharray': '10px 10px' + }, + orientation: 'horizontal', + x_axis: { + axis: 1, + decoupled: true + }, + y_axis: { + axis: 1, + decoupled: true + }, + offset: 0 + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Require that orientation be "horizontal" or "vertical" only + if ([ + 'horizontal', + 'vertical' + ].indexOf(layout.orientation) === -1) { + layout.orientation = 'horizontal'; + } + // Vars for storing the data generated line + /** @member {Array} */ + this.data = []; + /** @member {d3.svg.line} */ + this.line = null; + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + /** + * Implement the main render function + */ + this.render = function () { + // Several vars needed to be in scope + var panel = this.parent; + var x_scale = 'x_scale'; + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + var x_extent = 'x_extent'; + var y_extent = 'y' + this.layout.y_axis.axis + '_extent'; + var x_range = 'x_range'; + var y_range = 'y' + this.layout.y_axis.axis + '_range'; + // Generate data using extents depending on orientation + if (this.layout.orientation === 'horizontal') { + this.data = [ + { + x: panel[x_extent][0], + y: this.layout.offset + }, + { + x: panel[x_extent][1], + y: this.layout.offset + } + ]; + } else { + this.data = [ + { + x: this.layout.offset, + y: panel[y_extent][0] + }, + { + x: this.layout.offset, + y: panel[y_extent][1] + } + ]; } - return; - }); - }); - }; - - // Recursive function to space labels apart immediately after initial render - // Adapted from thudfactor's fiddle here: https://jsfiddle.net/thudfactor/HdwTH/ - // TODO: Make labels also aware of data elements - this.separate_labels = function(){ - this.seperate_iterations++; - var data_layer = this; - var alpha = 0.5; - var spacing = this.layout.label.spacing; - var again = false; - data_layer.label_texts.each(function () { - var a = this; - var da = d3.select(a); - var y1 = da.attr("y"); - data_layer.label_texts.each(function () { - var b = this; - // a & b are the same element and don't collide. - if (a === b) return; - var db = d3.select(b); - // a & b are on opposite sides of the chart and - // don't collide - if (da.attr("text-anchor") !== db.attr("text-anchor")) return; - // Determine if the bounding rects for the two text elements collide - var abound = da.node().getBoundingClientRect(); - var bbound = db.node().getBoundingClientRect(); - var collision = abound.left < bbound.left + bbound.width + (2*spacing) && - abound.left + abound.width + (2*spacing) > bbound.left && - abound.top < bbound.top + bbound.height + (2*spacing) && - abound.height + abound.top + (2*spacing) > bbound.top; - if (!collision) return; - again = true; - // If the labels collide, we'll push each - // of the two labels up and down a little bit. - var y2 = db.attr("y"); - var sign = abound.top < bbound.top ? 1 : -1; - var adjust = sign * alpha; - var new_a_y = +y1 - adjust; - var new_b_y = +y2 + adjust; - // Keep new values from extending outside the data layer - var min_y = 2 * spacing; - var max_y = data_layer.parent.layout.height - data_layer.parent.layout.margin.top - data_layer.parent.layout.margin.bottom - (2 * spacing); - var delta; - if (new_a_y - (abound.height/2) < min_y){ - delta = +y1 - new_a_y; - new_a_y = +y1; - new_b_y += delta; - } else if (new_b_y - (bbound.height/2) < min_y){ - delta = +y2 - new_b_y; - new_b_y = +y2; - new_a_y += delta; - } - if (new_a_y + (abound.height/2) > max_y){ - delta = new_a_y - +y1; - new_a_y = +y1; - new_b_y -= delta; - } else if (new_b_y + (bbound.height/2) > max_y){ - delta = new_b_y - +y2; - new_b_y = +y2; - new_a_y -= delta; - } - da.attr("y",new_a_y); - db.attr("y",new_b_y); - }); + // Join data to the line selection + var selection = this.svg.group.selectAll('path.lz-data_layer-line').data([this.data]); + // Create path element, apply class + this.path = selection.enter().append('path').attr('class', 'lz-data_layer-line'); + // Generate the line + this.line = d3.svg.line().x(function (d, i) { + var x = parseFloat(panel[x_scale](d['x'])); + return isNaN(x) ? panel[x_range][i] : x; + }).y(function (d, i) { + var y = parseFloat(panel[y_scale](d['y'])); + return isNaN(y) ? panel[y_range][i] : y; + }).interpolate('linear'); + // Apply line and style + if (this.canTransition()) { + selection.transition().duration(this.layout.transition.duration || 0).ease(this.layout.transition.ease || 'cubic-in-out').attr('d', this.line).style(this.layout.style); + } else { + selection.attr('d', this.line).style(this.layout.style); + } + // Remove old elements as needed + selection.exit().remove(); + }; + return this; }); - if (again) { - // Adjust lines to follow the labels - if (data_layer.layout.label.lines){ - var label_elements = data_layer.label_texts[0]; - data_layer.label_lines.attr("y2",function(d,i) { - var label_line = d3.select(label_elements[i]); - return label_line.attr("y"); - }); - } - // After ~150 iterations we're probably beyond diminising returns, so stop recursing - if (this.seperate_iterations < 150){ - setTimeout(function(){ - this.separate_labels(); - }.bind(this), 1); + 'use strict'; + /********************* + Scatter Data Layer + Implements a standard scatter plot +*/ + LocusZoom.DataLayers.add('scatter', function (layout) { + // Define a default layout for this DataLayer type and merge it with the passed argument + this.DefaultLayout = { + point_size: 40, + point_shape: 'circle', + tooltip_positioning: 'horizontal', + color: '#888888', + fill_opacity: 1, + y_axis: { axis: 1 }, + id_field: 'id' + }; + layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout); + // Extra default for layout spacing + // Not in default layout since that would make the label attribute always present + if (layout.label && isNaN(layout.label.spacing)) { + layout.label.spacing = 4; } - } - }; - - // Implement the main render function - this.render = function(){ - - var data_layer = this; - var x_scale = "x_scale"; - var y_scale = "y"+this.layout.y_axis.axis+"_scale"; - - // Generate labels first (if defined) - if (this.layout.label){ - // Apply filters to generate a filtered data set - var filtered_data = this.data.filter(function(d){ - if (!data_layer.layout.label.filters){ - return true; + // Apply the arguments to set LocusZoom.DataLayer as the prototype + LocusZoom.DataLayer.apply(this, arguments); + // Reimplement the positionTooltip() method to be scatter-specific + this.positionTooltip = function (id) { + if (typeof id != 'string') { + throw 'Unable to position tooltip: id is not a string'; + } + if (!this.tooltips[id]) { + throw 'Unable to position tooltip: id does not point to a valid tooltip'; + } + var top, left, arrow_type, arrow_top, arrow_left; + var tooltip = this.tooltips[id]; + var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data); + var offset = Math.sqrt(point_size / Math.PI); + var arrow_width = 7; + // as defined in the default stylesheet + var stroke_width = 1; + // as defined in the default stylesheet + var border_radius = 6; + // as defined in the default stylesheet + var page_origin = this.getPageOrigin(); + var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]); + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]); + var tooltip_box = tooltip.selector.node().getBoundingClientRect(); + var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right); + if (this.layout.tooltip_positioning === 'vertical') { + // Position horizontally centered above the point + var offset_right = Math.max(tooltip_box.width / 2 - x_center, 0); + var offset_left = Math.max(tooltip_box.width / 2 + x_center - data_layer_width, 0); + left = page_origin.x + x_center - tooltip_box.width / 2 - offset_left + offset_right; + arrow_left = tooltip_box.width / 2 - arrow_width / 2 + offset_left - offset_right - offset; + // Position vertically above the point unless there's insufficient space, then go below + if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (y_center + offset)) { + top = page_origin.y + y_center - (offset + tooltip_box.height + stroke_width + arrow_width); + arrow_type = 'down'; + arrow_top = tooltip_box.height - stroke_width; + } else { + top = page_origin.y + y_center + offset + stroke_width + arrow_width; + arrow_type = 'up'; + arrow_top = 0 - stroke_width - arrow_width; + } } else { - // Start by assuming a match, run through all filters to test if not a match on any one - var match = true; - data_layer.layout.label.filters.forEach(function(filter){ - var field_value = (new LocusZoom.Data.Field(filter.field)).resolve(d); - if (isNaN(field_value)){ - match = false; - } else { - switch (filter.operator){ - case "<": - if (!(field_value < filter.value)){ match = false; } - break; - case "<=": - if (!(field_value <= filter.value)){ match = false; } - break; - case ">": - if (!(field_value > filter.value)){ match = false; } - break; - case ">=": - if (!(field_value >= filter.value)){ match = false; } - break; - case "=": - if (!(field_value === filter.value)){ match = false; } - break; - default: - // If we got here the operator is not valid, so the filter should fail - match = false; - break; + // Position horizontally on the left or the right depending on which side of the plot the point is on + if (x_center <= this.parent.layout.width / 2) { + left = page_origin.x + x_center + offset + arrow_width + stroke_width; + arrow_type = 'left'; + arrow_left = -1 * (arrow_width + stroke_width); + } else { + left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width; + arrow_type = 'right'; + arrow_left = tooltip_box.width - stroke_width; + } + // Position vertically centered unless we're at the top or bottom of the plot + data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom); + if (y_center - tooltip_box.height / 2 <= 0) { + // Too close to the top, push it down + top = page_origin.y + y_center - 1.5 * arrow_width - border_radius; + arrow_top = border_radius; + } else if (y_center + tooltip_box.height / 2 >= data_layer_height) { + // Too close to the bottom, pull it up + top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height; + arrow_top = tooltip_box.height - 2 * arrow_width - border_radius; + } else { + // vertically centered + top = page_origin.y + y_center - tooltip_box.height / 2; + arrow_top = tooltip_box.height / 2 - arrow_width; + } + } + // Apply positions to the main div + tooltip.selector.style('left', left + 'px').style('top', top + 'px'); + // Create / update position on arrow connecting tooltip to data + if (!tooltip.arrow) { + tooltip.arrow = tooltip.selector.append('div').style('position', 'absolute'); + } + tooltip.arrow.attr('class', 'lz-data_layer-tooltip-arrow_' + arrow_type).style('left', arrow_left + 'px').style('top', arrow_top + 'px'); + }; + // Function to flip labels from being anchored at the start of the text to the end + // Both to keep labels from running outside the data layer and also as a first + // pass on recursive separation + this.flip_labels = function () { + var data_layer = this; + var point_size = data_layer.resolveScalableParameter(data_layer.layout.point_size, {}); + var spacing = data_layer.layout.label.spacing; + var handle_lines = Boolean(data_layer.layout.label.lines); + var min_x = 2 * spacing; + var max_x = data_layer.parent.layout.width - data_layer.parent.layout.margin.left - data_layer.parent.layout.margin.right - 2 * spacing; + var flip = function (dn, dnl) { + var dnx = +dn.attr('x'); + var text_swing = 2 * spacing + 2 * Math.sqrt(point_size); + if (handle_lines) { + var dnlx2 = +dnl.attr('x2'); + var line_swing = spacing + 2 * Math.sqrt(point_size); + } + if (dn.style('text-anchor') === 'start') { + dn.style('text-anchor', 'end'); + dn.attr('x', dnx - text_swing); + if (handle_lines) { + dnl.attr('x2', dnlx2 - line_swing); + } + } else { + dn.style('text-anchor', 'start'); + dn.attr('x', dnx + text_swing); + if (handle_lines) { + dnl.attr('x2', dnlx2 + line_swing); + } + } + }; + // Flip any going over the right edge from the right side to the left side + // (all labels start on the right side) + data_layer.label_texts.each(function (d, i) { + var a = this; + var da = d3.select(a); + var dax = +da.attr('x'); + var abound = da.node().getBoundingClientRect(); + if (dax + abound.width + spacing > max_x) { + var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null; + flip(da, dal); + } + }); + // Second pass to flip any others that haven't flipped yet if they collide with another label + data_layer.label_texts.each(function (d, i) { + var a = this; + var da = d3.select(a); + if (da.style('text-anchor') === 'end') + return; + var dax = +da.attr('x'); + var abound = da.node().getBoundingClientRect(); + var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null; + data_layer.label_texts.each(function () { + var b = this; + var db = d3.select(b); + var bbound = db.node().getBoundingClientRect(); + var collision = abound.left < bbound.left + bbound.width + 2 * spacing && abound.left + abound.width + 2 * spacing > bbound.left && abound.top < bbound.top + bbound.height + 2 * spacing && abound.height + abound.top + 2 * spacing > bbound.top; + if (collision) { + flip(da, dal); + // Double check that this flip didn't push the label past min_x. If it did, immediately flip back. + dax = +da.attr('x'); + if (dax - abound.width - spacing < min_x) { + flip(da, dal); } } + return; + }); + }); + }; + // Recursive function to space labels apart immediately after initial render + // Adapted from thudfactor's fiddle here: https://jsfiddle.net/thudfactor/HdwTH/ + // TODO: Make labels also aware of data elements + this.separate_labels = function () { + this.seperate_iterations++; + var data_layer = this; + var alpha = 0.5; + var spacing = this.layout.label.spacing; + var again = false; + data_layer.label_texts.each(function () { + var a = this; + var da = d3.select(a); + var y1 = da.attr('y'); + data_layer.label_texts.each(function () { + var b = this; + // a & b are the same element and don't collide. + if (a === b) + return; + var db = d3.select(b); + // a & b are on opposite sides of the chart and + // don't collide + if (da.attr('text-anchor') !== db.attr('text-anchor')) + return; + // Determine if the bounding rects for the two text elements collide + var abound = da.node().getBoundingClientRect(); + var bbound = db.node().getBoundingClientRect(); + var collision = abound.left < bbound.left + bbound.width + 2 * spacing && abound.left + abound.width + 2 * spacing > bbound.left && abound.top < bbound.top + bbound.height + 2 * spacing && abound.height + abound.top + 2 * spacing > bbound.top; + if (!collision) + return; + again = true; + // If the labels collide, we'll push each + // of the two labels up and down a little bit. + var y2 = db.attr('y'); + var sign = abound.top < bbound.top ? 1 : -1; + var adjust = sign * alpha; + var new_a_y = +y1 - adjust; + var new_b_y = +y2 + adjust; + // Keep new values from extending outside the data layer + var min_y = 2 * spacing; + var max_y = data_layer.parent.layout.height - data_layer.parent.layout.margin.top - data_layer.parent.layout.margin.bottom - 2 * spacing; + var delta; + if (new_a_y - abound.height / 2 < min_y) { + delta = +y1 - new_a_y; + new_a_y = +y1; + new_b_y += delta; + } else if (new_b_y - bbound.height / 2 < min_y) { + delta = +y2 - new_b_y; + new_b_y = +y2; + new_a_y += delta; + } + if (new_a_y + abound.height / 2 > max_y) { + delta = new_a_y - +y1; + new_a_y = +y1; + new_b_y -= delta; + } else if (new_b_y + bbound.height / 2 > max_y) { + delta = new_b_y - +y2; + new_b_y = +y2; + new_a_y -= delta; + } + da.attr('y', new_a_y); + db.attr('y', new_b_y); }); - return match; + }); + if (again) { + // Adjust lines to follow the labels + if (data_layer.layout.label.lines) { + var label_elements = data_layer.label_texts[0]; + data_layer.label_lines.attr('y2', function (d, i) { + var label_line = d3.select(label_elements[i]); + return label_line.attr('y'); + }); + } + // After ~150 iterations we're probably beyond diminising returns, so stop recursing + if (this.seperate_iterations < 150) { + setTimeout(function () { + this.separate_labels(); + }.bind(this), 1); + } } - }); - // Render label groups - var self = this; - this.label_groups = this.svg.group - .selectAll("g.lz-data_layer-" + this.layout.type + "-label") - .data(filtered_data, function(d){ return d[self.layout.id_field] + "_label"; }); - this.label_groups.enter() - .append("g") - .attr("class", "lz-data_layer-"+ this.layout.type + "-label"); - // Render label texts - if (this.label_texts){ this.label_texts.remove(); } - this.label_texts = this.label_groups.append("text") - .attr("class", "lz-data_layer-" + this.layout.type + "-label"); - this.label_texts - .text(function(d){ - return LocusZoom.parseFields(d, data_layer.layout.label.text || ""); - }) - .style(data_layer.layout.label.style || {}) - .attr({ - "x": function(d){ - var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]) - + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d)) - + data_layer.layout.label.spacing; - if (isNaN(x)){ x = -1000; } - return x; - }, - "y": function(d){ - var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]); - if (isNaN(y)){ y = -1000; } - return y; - }, - "text-anchor": function(){ - return "start"; + }; + // Implement the main render function + this.render = function () { + var data_layer = this; + var x_scale = 'x_scale'; + var y_scale = 'y' + this.layout.y_axis.axis + '_scale'; + // Generate labels first (if defined) + if (this.layout.label) { + // Apply filters to generate a filtered data set + var filtered_data = this.data.filter(function (d) { + if (!data_layer.layout.label.filters) { + return true; + } else { + // Start by assuming a match, run through all filters to test if not a match on any one + var match = true; + data_layer.layout.label.filters.forEach(function (filter) { + var field_value = new LocusZoom.Data.Field(filter.field).resolve(d); + if (isNaN(field_value)) { + match = false; + } else { + switch (filter.operator) { + case '<': + if (!(field_value < filter.value)) { + match = false; + } + break; + case '<=': + if (!(field_value <= filter.value)) { + match = false; + } + break; + case '>': + if (!(field_value > filter.value)) { + match = false; + } + break; + case '>=': + if (!(field_value >= filter.value)) { + match = false; + } + break; + case '=': + if (!(field_value === filter.value)) { + match = false; + } + break; + default: + // If we got here the operator is not valid, so the filter should fail + match = false; + break; + } + } + }); + return match; + } + }); + // Render label groups + var self = this; + this.label_groups = this.svg.group.selectAll('g.lz-data_layer-' + this.layout.type + '-label').data(filtered_data, function (d) { + return d[self.layout.id_field] + '_label'; + }); + this.label_groups.enter().append('g').attr('class', 'lz-data_layer-' + this.layout.type + '-label'); + // Render label texts + if (this.label_texts) { + this.label_texts.remove(); } - }); - // Render label lines - if (data_layer.layout.label.lines){ - if (this.label_lines){ this.label_lines.remove(); } - this.label_lines = this.label_groups.append("line") - .attr("class", "lz-data_layer-" + this.layout.type + "-label"); - this.label_lines - .style(data_layer.layout.label.lines.style || {}) - .attr({ - "x1": function(d){ - var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]); - if (isNaN(x)){ x = -1000; } + this.label_texts = this.label_groups.append('text').attr('class', 'lz-data_layer-' + this.layout.type + '-label'); + this.label_texts.text(function (d) { + return LocusZoom.parseFields(d, data_layer.layout.label.text || ''); + }).style(data_layer.layout.label.style || {}).attr({ + 'x': function (d) { + var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]) + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d)) + data_layer.layout.label.spacing; + if (isNaN(x)) { + x = -1000; + } return x; }, - "y1": function(d){ + 'y': function (d) { var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]); - if (isNaN(y)){ y = -1000; } + if (isNaN(y)) { + y = -1000; + } return y; }, - "x2": function(d){ - var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]) - + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d)) - + (data_layer.layout.label.spacing/2); - if (isNaN(x)){ x = -1000; } - return x; - }, - "y2": function(d){ - var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]); - if (isNaN(y)){ y = -1000; } - return y; + 'text-anchor': function () { + return 'start'; } }); - } - // Remove labels when they're no longer in the filtered data set - this.label_groups.exit().remove(); - } - - // Generate main scatter data elements - var selection = this.svg.group - .selectAll("path.lz-data_layer-" + this.layout.type) - .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this)); - - // Create elements, apply class, ID, and initial position - var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height; - selection.enter() - .append("path") - .attr("class", "lz-data_layer-" + this.layout.type) - .attr("id", function(d){ return this.getElementId(d); }.bind(this)) - .attr("transform", "translate(0," + initial_y + ")"); - - // Generate new values (or functions for them) for position, color, size, and shape - var transform = function(d) { - var x = this.parent[x_scale](d[this.layout.x_axis.field]); - var y = this.parent[y_scale](d[this.layout.y_axis.field]); - if (isNaN(x)){ x = -1000; } - if (isNaN(y)){ y = -1000; } - return "translate(" + x + "," + y + ")"; - }.bind(this); - - var fill = function(d){ return this.resolveScalableParameter(this.layout.color, d); }.bind(this); - var fill_opacity = function(d){ return this.resolveScalableParameter(this.layout.fill_opacity, d); }.bind(this); - - var shape = d3.svg.symbol() - .size(function(d){ return this.resolveScalableParameter(this.layout.point_size, d); }.bind(this)) - .type(function(d){ return this.resolveScalableParameter(this.layout.point_shape, d); }.bind(this)); - - // Apply position and color, using a transition if necessary - - if (this.canTransition()){ - selection - .transition() - .duration(this.layout.transition.duration || 0) - .ease(this.layout.transition.ease || "cubic-in-out") - .attr("transform", transform) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity) - .attr("d", shape); - } else { - selection - .attr("transform", transform) - .attr("fill", fill) - .attr("fill-opacity", fill_opacity) - .attr("d", shape); - } - - // Remove old elements as needed - selection.exit().remove(); - - // Apply default event emitters to selection - selection.on("click.event_emitter", function(element){ - this.parent.emit("element_clicked", element); - this.parent_plot.emit("element_clicked", element); - }.bind(this)); - - // Apply mouse behaviors - this.applyBehaviors(selection); - - // Apply method to keep labels from overlapping each other - if (this.layout.label){ - this.flip_labels(); - this.seperate_iterations = 0; - this.separate_labels(); - // Extend mouse behaviors to labels - this.applyBehaviors(this.label_texts); - } - - }; - - // Method to set a passed element as the LD reference in the plot-level state - this.makeLDReference = function(element){ - var ref = null; - if (typeof element == "undefined"){ - throw("makeLDReference requires one argument of any type"); - } else if (typeof element == "object"){ - if (this.layout.id_field && typeof element[this.layout.id_field] != "undefined"){ - ref = element[this.layout.id_field].toString(); - } else if (typeof element["id"] != "undefined"){ - ref = element["id"].toString(); - } else { - ref = element.toString(); - } - } else { - ref = element.toString(); - } - this.parent_plot.applyState({ ldrefvar: ref }); - }; - - return this; - -}); - -/** + // Render label lines + if (data_layer.layout.label.lines) { + if (this.label_lines) { + this.label_lines.remove(); + } + this.label_lines = this.label_groups.append('line').attr('class', 'lz-data_layer-' + this.layout.type + '-label'); + this.label_lines.style(data_layer.layout.label.lines.style || {}).attr({ + 'x1': function (d) { + var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]); + if (isNaN(x)) { + x = -1000; + } + return x; + }, + 'y1': function (d) { + var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]); + if (isNaN(y)) { + y = -1000; + } + return y; + }, + 'x2': function (d) { + var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]) + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d)) + data_layer.layout.label.spacing / 2; + if (isNaN(x)) { + x = -1000; + } + return x; + }, + 'y2': function (d) { + var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]); + if (isNaN(y)) { + y = -1000; + } + return y; + } + }); + } + // Remove labels when they're no longer in the filtered data set + this.label_groups.exit().remove(); + } + // Generate main scatter data elements + var selection = this.svg.group.selectAll('path.lz-data_layer-' + this.layout.type).data(this.data, function (d) { + return d[this.layout.id_field]; + }.bind(this)); + // Create elements, apply class, ID, and initial position + var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height; + selection.enter().append('path').attr('class', 'lz-data_layer-' + this.layout.type).attr('id', function (d) { + return this.getElementId(d); + }.bind(this)).attr('transform', 'translate(0,' + initial_y + ')'); + // Generate new values (or functions for them) for position, color, size, and shape + var transform = function (d) { + var x = this.parent[x_scale](d[this.layout.x_axis.field]); + var y = this.parent[y_scale](d[this.layout.y_axis.field]); + if (isNaN(x)) { + x = -1000; + } + if (isNaN(y)) { + y = -1000; + } + return 'translate(' + x + ',' + y + ')'; + }.bind(this); + var fill = function (d) { + return this.resolveScalableParameter(this.layout.color, d); + }.bind(this); + var fill_opacity = function (d) { + return this.resolveScalableParameter(this.layout.fill_opacity, d); + }.bind(this); + var shape = d3.svg.symbol().size(function (d) { + return this.resolveScalableParameter(this.layout.point_size, d); + }.bind(this)).type(function (d) { + return this.resolveScalableParameter(this.layout.point_shape, d); + }.bind(this)); + // Apply position and color, using a transition if necessary + if (this.canTransition()) { + selection.transition().duration(this.layout.transition.duration || 0).ease(this.layout.transition.ease || 'cubic-in-out').attr('transform', transform).attr('fill', fill).attr('fill-opacity', fill_opacity).attr('d', shape); + } else { + selection.attr('transform', transform).attr('fill', fill).attr('fill-opacity', fill_opacity).attr('d', shape); + } + // Remove old elements as needed + selection.exit().remove(); + // Apply default event emitters to selection + selection.on('click.event_emitter', function (element) { + this.parent.emit('element_clicked', element); + this.parent_plot.emit('element_clicked', element); + }.bind(this)); + // Apply mouse behaviors + this.applyBehaviors(selection); + // Apply method to keep labels from overlapping each other + if (this.layout.label) { + this.flip_labels(); + this.seperate_iterations = 0; + this.separate_labels(); + // Apply default event emitters to selection + this.label_texts.on('click.event_emitter', function (element) { + this.parent.emit('element_clicked', element); + this.parent_plot.emit('element_clicked', element); + }.bind(this)); + // Extend mouse behaviors to labels + this.applyBehaviors(this.label_texts); + } + }; + // Method to set a passed element as the LD reference in the plot-level state + this.makeLDReference = function (element) { + var ref = null; + if (typeof element == 'undefined') { + throw 'makeLDReference requires one argument of any type'; + } else if (typeof element == 'object') { + if (this.layout.id_field && typeof element[this.layout.id_field] != 'undefined') { + ref = element[this.layout.id_field].toString(); + } else if (typeof element['id'] != 'undefined') { + ref = element['id'].toString(); + } else { + ref = element.toString(); + } + } else { + ref = element.toString(); + } + this.parent_plot.applyState({ ldrefvar: ref }); + }; + return this; + }); + /** * A scatter plot in which the x-axis represents categories, rather than individual positions. * For example, this can be used by PheWAS plots to show related groups. This plot allows the categories to be * determined dynamically when data is first loaded. @@ -5264,139 +5202,195 @@ LocusZoom.DataLayers.add("scatter", function(layout){ * @class LocusZoom.DataLayers.category_scatter * @augments LocusZoom.DataLayers.scatter */ -LocusZoom.DataLayers.extend("scatter", "category_scatter", { - /** + LocusZoom.DataLayers.extend('scatter', 'category_scatter', { + /** * This plot layer makes certain assumptions about the data passed in. Transform the raw array of records from * the datasource to prepare it for plotting, as follows: * 1. The scatter plot assumes that all records are given in sequence (pre-grouped by `category_field`) * 2. It assumes that all records have an x coordinate for individual plotting * @private */ - _prepareData: function() { - var xField = this.layout.x_axis.field || "x"; - // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color - var category_field = this.layout.x_axis.category_field; - if (!category_field) { - throw "Layout for " + this.layout.id + " must specify category_field"; - } - // Sort the data so that things in the same category are adjacent (case-insensitive by specified field) - var sourceData = this.data - .sort(function(a, b) { - var ak = a[category_field]; - var bk = b[category_field]; - var av = ak.toString ? ak.toString().toLowerCase() : ak; - var bv = bk.toString ? bk.toString().toLowerCase() : bk; - return (av === bv) ? 0 : (av < bv ? -1 : 1);}); - sourceData.forEach(function(d, i){ - // Implementation detail: Scatter plot requires specifying an x-axis value, and most datasources do not - // specify plotting positions. If a point is missing this field, fill in a synthetic value. - d[xField] = d[xField] || i; - }); - return sourceData; - }, - - /** - * Identify the unique categories on the plot, and update the layout with an appropriate color scheme - * + _prepareData: function () { + var xField = this.layout.x_axis.field || 'x'; + // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color + var category_field = this.layout.x_axis.category_field; + if (!category_field) { + throw 'Layout for ' + this.layout.id + ' must specify category_field'; + } + // Sort the data so that things in the same category are adjacent (case-insensitive by specified field) + var sourceData = this.data.sort(function (a, b) { + var ak = a[category_field]; + var bk = b[category_field]; + var av = ak.toString ? ak.toString().toLowerCase() : ak; + var bv = bk.toString ? bk.toString().toLowerCase() : bk; + return av === bv ? 0 : av < bv ? -1 : 1; + }); + sourceData.forEach(function (d, i) { + // Implementation detail: Scatter plot requires specifying an x-axis value, and most datasources do not + // specify plotting positions. If a point is missing this field, fill in a synthetic value. + d[xField] = d[xField] || i; + }); + return sourceData; + }, + /** + * Identify the unique categories on the plot, and update the layout with an appropriate color scheme. * Also identify the min and max x value associated with the category, which will be used to generate ticks * @private * @returns {Object.} Series of entries used to build category name ticks {category_name: [min_x, max_x]} */ - _generateCategoryBounds: function() { - // TODO: API may return null values in category_field; should we add placeholder category label? - // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color - var category_field = this.layout.x_axis.category_field; - var xField = this.layout.x_axis.field || "x"; - var uniqueCategories = {}; - this.data.forEach(function(item) { - var category = item[category_field]; - var x = item[xField]; - var bounds = uniqueCategories[category] || [x, x]; - uniqueCategories[category] = [Math.min(bounds[0], x), Math.max(bounds[1], x)]; - }); - - var categoryNames = Object.keys(uniqueCategories); - // Construct a color scale with a sufficient number of visually distinct colors - // TODO: This will break for more than 20 categories in a single API response payload for a single PheWAS plot - var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20; - var colors = color_scale().range().slice(0, categoryNames.length); // List of hex values, should be of same length as categories array - - this.layout.color.parameters.categories = categoryNames; - this.layout.color.parameters.values = colors; - return uniqueCategories; - }, - - /** + _generateCategoryBounds: function () { + // TODO: API may return null values in category_field; should we add placeholder category label? + // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color + var category_field = this.layout.x_axis.category_field; + var xField = this.layout.x_axis.field || 'x'; + var uniqueCategories = {}; + this.data.forEach(function (item) { + var category = item[category_field]; + var x = item[xField]; + var bounds = uniqueCategories[category] || [ + x, + x + ]; + uniqueCategories[category] = [ + Math.min(bounds[0], x), + Math.max(bounds[1], x) + ]; + }); + var categoryNames = Object.keys(uniqueCategories); + this._setDynamicColorScheme(categoryNames); + return uniqueCategories; + }, + /** + * Automatically define a color scheme for the layer based on data returned from the server. + * If part of the color scheme has been specified, it will fill in remaining missing information. + * + * There are three scenarios: + * 1. The layout does not specify either category names or (color) values. Dynamically build both based on + * the data and update the layout. + * 2. The layout specifies colors, but not categories. Use that exact color information provided, and dynamically + * determine what categories are present in the data. (cycle through the available colors, reusing if there + * are a lot of categories) + * 3. The layout specifies exactly what colors and categories to use (and they match the data!). This is useful to + * specify an explicit mapping between color scheme and category names, when you want to be sure that the + * plot matches a standard color scheme. + * (If the layout specifies categories that do not match the data, the user specified categories will be ignored) + * + * This method will only act if the layout defines a `categorical_bin` scale function for coloring. It may be + * overridden in a subclass to suit other types of coloring methods. + * + * @param {String[]} categoryNames + * @private + */ + _setDynamicColorScheme: function (categoryNames) { + var colorParams = this.layout.color.parameters; + var baseParams = this._base_layout.color.parameters; + // If the layout does not use a supported coloring scheme, or is already complete, this method should do nothing + if (this.layout.color.scale_function !== 'categorical_bin') { + throw 'This layer requires that coloring be specified as a `categorical_bin`'; + } + if (baseParams.categories.length && baseParams.values.length) { + // If there are preset category/color combos, make sure that they apply to the actual dataset + var parameters_categories_hash = {}; + baseParams.categories.forEach(function (category) { + parameters_categories_hash[category] = 1; + }); + if (categoryNames.every(function (name) { + return parameters_categories_hash.hasOwnProperty(name); + })) { + // The layout doesn't have to specify categories in order, but make sure they are all there + colorParams.categories = baseParams.categories; + } else { + colorParams.categories = categoryNames; + } + } else { + colorParams.categories = categoryNames; + } + // Prefer user-specified colors if provided. Make sure that there are enough colors for all the categories. + var colors; + if (baseParams.values.length) { + colors = baseParams.values; + } else { + var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20; + colors = color_scale().range(); + } + while (colors.length < categoryNames.length) { + colors = colors.concat(colors); + } + colors = colors.slice(0, categoryNames.length); + // List of hex values, should be of same length as categories array + colorParams.values = colors; + }, + /** * * @param dimension * @param {Object} [config] Parameters that customize how ticks are calculated (not style) * @param {('left'|'center'|'right')} [config.position='left'] Align ticks with the center or edge of category * @returns {Array} */ - getTicks: function(dimension, config) { // Overrides parent method - if (["x", "y"].indexOf(dimension) === -1) { - throw "Invalid dimension identifier"; - } - var position = config.position || "left"; - if (["left", "center", "right"].indexOf(position) === -1) { - throw "Invalid tick position"; - } - - var categoryBounds = this._categories; - if (!categoryBounds || !Object.keys(categoryBounds).length) { - return []; - } - - if (dimension === "y") { - return []; - } - - if (dimension === "x") { - // If colors have been defined by this layer, use them to make tick colors match scatterplot point colors - var knownColors = this.layout.color.parameters.values || []; - - return Object.keys(categoryBounds).map(function (category, index) { - var bounds = categoryBounds[category]; - var xPos; - - switch(position) { - case "left": - xPos = bounds[0]; - break; - case "center": - // Center tick under one or many elements as appropriate - var diff = bounds[1] - bounds[0]; - xPos = bounds[0] + (diff !== 0 ? diff : bounds[0]) / 2; - break; - case "right": - xPos = bounds[1]; - break; + getTicks: function (dimension, config) { + // Overrides parent method + if ([ + 'x', + 'y' + ].indexOf(dimension) === -1) { + throw 'Invalid dimension identifier'; } - return { - x: xPos, - text: category, - style: { - "fill": knownColors[index] || "#000000" - } - }; - }); - } - }, - - applyCustomDataMethods: function() { - this.data = this._prepareData(); - /** + var position = config.position || 'left'; + if ([ + 'left', + 'center', + 'right' + ].indexOf(position) === -1) { + throw 'Invalid tick position'; + } + var categoryBounds = this._categories; + if (!categoryBounds || !Object.keys(categoryBounds).length) { + return []; + } + if (dimension === 'y') { + return []; + } + if (dimension === 'x') { + // If colors have been defined by this layer, use them to make tick colors match scatterplot point colors + var knownCategories = this.layout.color.parameters.categories || []; + var knownColors = this.layout.color.parameters.values || []; + return Object.keys(categoryBounds).map(function (category, index) { + var bounds = categoryBounds[category]; + var xPos; + switch (position) { + case 'left': + xPos = bounds[0]; + break; + case 'center': + // Center tick under one or many elements as appropriate + var diff = bounds[1] - bounds[0]; + xPos = bounds[0] + (diff !== 0 ? diff : bounds[0]) / 2; + break; + case 'right': + xPos = bounds[1]; + break; + } + return { + x: xPos, + text: category, + style: { 'fill': knownColors[knownCategories.indexOf(category)] || '#000000' } + }; + }); + } + }, + applyCustomDataMethods: function () { + this.data = this._prepareData(); + /** * Define category names and extents (boundaries) for plotting. TODO: properties in constructor * @member {Object.} Category names and extents, in the form {category_name: [min_x, max_x]} */ - this._categories = this._generateCategoryBounds(); - return this; - } -}); -/* global LocusZoom */ -"use strict"; - -/** + this._categories = this._generateCategoryBounds(); + return this; + } + }); + /* global LocusZoom */ + 'use strict'; + /** * * LocusZoom has various singleton objects that are used for registering functions or classes. * These objects provide safe, standard methods to redefine or delete existing functions/classes @@ -5404,53 +5398,47 @@ LocusZoom.DataLayers.extend("scatter", "category_scatter", { * * @namespace Singletons */ - - -/* + /* * The Collection of "Known" Data Sources. This registry is used internally by the `DataSources` class * @class * @static */ -LocusZoom.KnownDataSources = (function() { - /** @lends LocusZoom.KnownDataSources */ - var obj = {}; - /* @member {function[]} */ - var sources = []; - - var findSourceByName = function(x) { - for(var i=0; i 1) { - return function(x) { - var val = x; - for(var i = 0; i 1) { + return function (x) { + var val = x; + for (var i = 0; i < funs.length; i++) { + val = parseTrans(funs[i])(val); + } + return val; + }; + } + return null; + }; + /** * Retrieve a transformation function by name * @param {String} name The name of the transformation function to retrieve. May optionally be prefixed with a * pipe (`|`) when chaining multiple transformation functions. * @returns {function} The constructor for the transformation function */ - obj.get = function(name) { - if (name && name.substring(0,1)==="|") { - return parseTransString(name); - } else { - return parseTrans(name); - } - }; - /** + obj.get = function (name) { + if (name && name.substring(0, 1) === '|') { + return parseTransString(name); + } else { + return parseTrans(name); + } + }; + /** * Internal logic that registers a transformation function * @protected * @param {String} name * @param {function} fn */ - obj.set = function(name, fn) { - if (name.substring(0,1)==="|") { - throw("transformation name should not start with a pipe"); - } else { - if (fn) { - transformations[name] = fn; - } else { - delete transformations[name]; - } - } - }; - - /** + obj.set = function (name, fn) { + if (name.substring(0, 1) === '|') { + throw 'transformation name should not start with a pipe'; + } else { + if (fn) { + transformations[name] = fn; + } else { + delete transformations[name]; + } + } + }; + /** * Register a transformation function * @param {String} name * @param {function} fn A transformation function (should accept one argument with the value) */ - obj.add = function(name, fn) { - if (transformations[name]) { - throw("transformation already exists with name: " + name); - } else { - obj.set(name, fn); - } - }; - /** + obj.add = function (name, fn) { + if (transformations[name]) { + throw 'transformation already exists with name: ' + name; + } else { + obj.set(name, fn); + } + }; + /** * List the names of all registered transformation functions * @returns {String[]} */ - obj.list = function() { - return Object.keys(transformations); - }; - - return obj; -})(); - -/** + obj.list = function () { + return Object.keys(transformations); + }; + return obj; + }(); + /** * Return the -log (base 10) * @function neglog10 */ -LocusZoom.TransformationFunctions.add("neglog10", function(x) { - if (isNaN(x) || x <= 0){ return null; } - return -Math.log(x) / Math.LN10; -}); - -/** + LocusZoom.TransformationFunctions.add('neglog10', function (x) { + if (isNaN(x) || x <= 0) { + return null; + } + return -Math.log(x) / Math.LN10; + }); + /** * Convert a number from logarithm to scientific notation. Useful for, eg, a datasource that returns -log(p) by default * @function logtoscinotation */ -LocusZoom.TransformationFunctions.add("logtoscinotation", function(x) { - if (isNaN(x)){ return "NaN"; } - if (x === 0){ return "1"; } - var exp = Math.ceil(x); - var diff = exp - x; - var base = Math.pow(10, diff); - if (exp === 1){ - return (base / 10).toFixed(4); - } else if (exp === 2){ - return (base / 100).toFixed(3); - } else { - return base.toFixed(2) + " × 10^-" + exp; - } -}); - -/** + LocusZoom.TransformationFunctions.add('logtoscinotation', function (x) { + if (isNaN(x)) { + return 'NaN'; + } + if (x === 0) { + return '1'; + } + var exp = Math.ceil(x); + var diff = exp - x; + var base = Math.pow(10, diff); + if (exp === 1) { + return (base / 10).toFixed(4); + } else if (exp === 2) { + return (base / 100).toFixed(3); + } else { + return base.toFixed(2) + ' \xD7 10^-' + exp; + } + }); + /** * Represent a number in scientific notation * @function scinotation * @param {Number} x * @returns {String} */ -LocusZoom.TransformationFunctions.add("scinotation", function(x) { - if (isNaN(x)){ return "NaN"; } - if (x === 0){ return "0"; } - var log; - if (Math.abs(x) > 1){ - log = Math.ceil(Math.log(x) / Math.LN10); - } else { - log = Math.floor(Math.log(x) / Math.LN10); - } - if (Math.abs(log) <= 3){ - return x.toFixed(3); - } else { - return x.toExponential(2).replace("+", "").replace("e", " × 10^"); - } -}); - -/** + LocusZoom.TransformationFunctions.add('scinotation', function (x) { + if (isNaN(x)) { + return 'NaN'; + } + if (x === 0) { + return '0'; + } + var log; + if (Math.abs(x) > 1) { + log = Math.ceil(Math.log(x) / Math.LN10); + } else { + log = Math.floor(Math.log(x) / Math.LN10); + } + if (Math.abs(log) <= 3) { + return x.toFixed(3); + } else { + return x.toExponential(2).replace('+', '').replace('e', ' \xD7 10^'); + } + }); + /** * URL-encode the provided text, eg for constructing hyperlinks * @function urlencode * @param {String} str */ -LocusZoom.TransformationFunctions.add("urlencode", function(str) { - return encodeURIComponent(str); -}); - -/** + LocusZoom.TransformationFunctions.add('urlencode', function (str) { + return encodeURIComponent(str); + }); + /** * HTML-escape user entered values for use in constructed HTML fragments * * For example, this filter can be used on tooltips with custom HTML display * @function htmlescape * @param {String} str HTML-escape the provided value */ -LocusZoom.TransformationFunctions.add("htmlescape", function(str) { - if ( !str ) { - return ""; - } - str = str + ""; - - return str.replace( /['"<>&`]/g, function( s ) { - switch ( s ) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - case "`": - return "`"; - } - }); -}); - -/** + LocusZoom.TransformationFunctions.add('htmlescape', function (str) { + if (!str) { + return ''; + } + str = str + ''; + return str.replace(/['"<>&`]/g, function (s) { + switch (s) { + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case '`': + return '`'; + } + }); + }); + /** * Singleton for accessing/storing functions that will convert arbitrary data points to values in a given scale * Useful for anything that needs to scale discretely with data (e.g. color, point size, etc.) * @@ -5750,12 +5729,11 @@ LocusZoom.TransformationFunctions.add("htmlescape", function(str) { * @class * @static */ -LocusZoom.ScaleFunctions = (function() { - /** @lends LocusZoom.ScaleFunctions */ - var obj = {}; - var functions = {}; - - /** + LocusZoom.ScaleFunctions = function () { + /** @lends LocusZoom.ScaleFunctions */ + var obj = {}; + var functions = {}; + /** * Find a scale function and return it. If parameters and values are passed, calls the function directly; otherwise * returns a callable. * @param {String} name @@ -5763,58 +5741,53 @@ LocusZoom.ScaleFunctions = (function() { * @param {*} [value] The value to operate on * @returns {*} */ - obj.get = function(name, parameters, value) { - if (!name) { - return null; - } else if (functions[name]) { - if (typeof parameters === "undefined" && typeof value === "undefined"){ - return functions[name]; - } else { - return functions[name](parameters, value); - } - } else { - throw("scale function [" + name + "] not found"); - } - }; - - /** + obj.get = function (name, parameters, value) { + if (!name) { + return null; + } else if (functions[name]) { + if (typeof parameters === 'undefined' && typeof value === 'undefined') { + return functions[name]; + } else { + return functions[name](parameters, value); + } + } else { + throw 'scale function [' + name + '] not found'; + } + }; + /** * @protected * @param {String} name The name of the function to set/unset * @param {Function} [fn] The function to register. If blank, removes this function name from the registry. */ - obj.set = function(name, fn) { - if (fn) { - functions[name] = fn; - } else { - delete functions[name]; - } - }; - - /** + obj.set = function (name, fn) { + if (fn) { + functions[name] = fn; + } else { + delete functions[name]; + } + }; + /** * Add a new scale function to the registry * @param {String} name The name of the scale function * @param {function} fn A scale function that accepts two parameters: an object of configuration and a value */ - obj.add = function(name, fn) { - if (functions[name]) { - throw("scale function already exists with name: " + name); - } else { - obj.set(name, fn); - } - }; - - /** + obj.add = function (name, fn) { + if (functions[name]) { + throw 'scale function already exists with name: ' + name; + } else { + obj.set(name, fn); + } + }; + /** * List the names of all registered scale functions * @returns {String[]} */ - obj.list = function() { - return Object.keys(functions); - }; - - return obj; -})(); - -/** + obj.list = function () { + return Object.keys(functions); + }; + return obj; + }(); + /** * Basic conditional function to evaluate the value of the input field and return based on equality. * @param {Object} parameters * @param {*} parameters.field_value The value against which to test the input value. @@ -5824,19 +5797,18 @@ LocusZoom.ScaleFunctions = (function() { * to match field_value. * @param {*} input value */ -LocusZoom.ScaleFunctions.add("if", function(parameters, input){ - if (typeof input == "undefined" || parameters.field_value !== input){ - if (typeof parameters.else != "undefined"){ - return parameters.else; - } else { - return null; - } - } else { - return parameters.then; - } -}); - -/** + LocusZoom.ScaleFunctions.add('if', function (parameters, input) { + if (typeof input == 'undefined' || parameters.field_value !== input) { + if (typeof parameters.else != 'undefined') { + return parameters.else; + } else { + return null; + } + } else { + return parameters.then; + } + }); + /** * Function to sort numerical values into bins based on numerical break points. Will only operate on numbers and * return null (or value of null_value parameter, if defined) if provided a non-numeric input value. Parameters: * @function numerical_bin @@ -5851,23 +5823,22 @@ LocusZoom.ScaleFunctions.add("if", function(parameters, input){ * @param {*} input value * @returns */ -LocusZoom.ScaleFunctions.add("numerical_bin", function(parameters, input){ - var breaks = parameters.breaks || []; - var values = parameters.values || []; - if (typeof input == "undefined" || input === null || isNaN(+input)){ - return (parameters.null_value ? parameters.null_value : null); - } - var threshold = breaks.reduce(function(prev, curr){ - if (+input < prev || (+input >= prev && +input < curr)){ - return prev; - } else { - return curr; - } - }); - return values[breaks.indexOf(threshold)]; -}); - -/** + LocusZoom.ScaleFunctions.add('numerical_bin', function (parameters, input) { + var breaks = parameters.breaks || []; + var values = parameters.values || []; + if (typeof input == 'undefined' || input === null || isNaN(+input)) { + return parameters.null_value ? parameters.null_value : null; + } + var threshold = breaks.reduce(function (prev, curr) { + if (+input < prev || +input >= prev && +input < curr) { + return prev; + } else { + return curr; + } + }); + return values[breaks.indexOf(threshold)]; + }); + /** * Function to sort values of any type into bins based on direct equality testing with a list of categories. * Will return null if provided an input value that does not match to a listed category. * @function categorical_bin @@ -5880,15 +5851,14 @@ LocusZoom.ScaleFunctions.add("numerical_bin", function(parameters, input){ * value in the categories parameter. * @param {*} parameters.null_value Value to return if the input value fails to match to any categories. Optional. */ -LocusZoom.ScaleFunctions.add("categorical_bin", function(parameters, value){ - if (typeof value == "undefined" || parameters.categories.indexOf(value) === -1){ - return (parameters.null_value ? parameters.null_value : null); - } else { - return parameters.values[parameters.categories.indexOf(value)]; - } -}); - -/** + LocusZoom.ScaleFunctions.add('categorical_bin', function (parameters, value) { + if (typeof value == 'undefined' || parameters.categories.indexOf(value) === -1) { + return parameters.null_value ? parameters.null_value : null; + } else { + return parameters.values[parameters.categories.indexOf(value)]; + } + }); + /** * Function for continuous interpolation of numerical values along a gradient with arbitrarily many break points. * @function interpolate * @parameters {Object} parameters @@ -5903,33 +5873,43 @@ LocusZoom.ScaleFunctions.add("categorical_bin", function(parameters, value){ * colors, shapes, etc. * @parameters {*} parameters.null_value */ -LocusZoom.ScaleFunctions.add("interpolate", function(parameters, input){ - var breaks = parameters.breaks || []; - var values = parameters.values || []; - var nullval = (parameters.null_value ? parameters.null_value : null); - if (breaks.length < 2 || breaks.length !== values.length){ return nullval; } - if (typeof input == "undefined" || input === null || isNaN(+input)){ return nullval; } - if (+input <= parameters.breaks[0]){ - return values[0]; - } else if (+input >= parameters.breaks[parameters.breaks.length-1]){ - return values[breaks.length-1]; - } else { - var upper_idx = null; - breaks.forEach(function(brk, idx){ - if (!idx){ return; } - if (breaks[idx-1] <= +input && breaks[idx] >= +input){ upper_idx = idx; } + LocusZoom.ScaleFunctions.add('interpolate', function (parameters, input) { + var breaks = parameters.breaks || []; + var values = parameters.values || []; + var nullval = parameters.null_value ? parameters.null_value : null; + if (breaks.length < 2 || breaks.length !== values.length) { + return nullval; + } + if (typeof input == 'undefined' || input === null || isNaN(+input)) { + return nullval; + } + if (+input <= parameters.breaks[0]) { + return values[0]; + } else if (+input >= parameters.breaks[parameters.breaks.length - 1]) { + return values[breaks.length - 1]; + } else { + var upper_idx = null; + breaks.forEach(function (brk, idx) { + if (!idx) { + return; + } + if (breaks[idx - 1] <= +input && breaks[idx] >= +input) { + upper_idx = idx; + } + }); + if (upper_idx === null) { + return nullval; + } + var normalized_input = (+input - breaks[upper_idx - 1]) / (breaks[upper_idx] - breaks[upper_idx - 1]); + if (!isFinite(normalized_input)) { + return nullval; + } + return d3.interpolate(values[upper_idx - 1], values[upper_idx])(normalized_input); + } }); - if (upper_idx === null){ return nullval; } - var normalized_input = (+input - breaks[upper_idx-1]) / (breaks[upper_idx] - breaks[upper_idx-1]); - if (!isFinite(normalized_input)){ return nullval; } - return d3.interpolate(values[upper_idx-1], values[upper_idx])(normalized_input); - } -}); - -/* global LocusZoom */ -"use strict"; - -/** + /* global LocusZoom */ + 'use strict'; + /** * A Dashboard is an HTML element used for presenting arbitrary user interface components. Dashboards are anchored * to either the entire Plot or to individual Panels. * @@ -5939,172 +5919,190 @@ LocusZoom.ScaleFunctions.add("interpolate", function(parameters, input){ * an overlay. * @class */ -LocusZoom.Dashboard = function(parent){ - // parent must be a locuszoom plot or panel - if (!(parent instanceof LocusZoom.Plot) && !(parent instanceof LocusZoom.Panel)){ - throw "Unable to create dashboard, parent must be a locuszoom plot or panel"; - } - /** @member {LocusZoom.Plot|LocusZoom.Panel} */ - this.parent = parent; - /** @member {String} */ - this.id = this.parent.getBaseId() + ".dashboard"; - /** @member {('plot'|'panel')} */ - this.type = (this.parent instanceof LocusZoom.Plot) ? "plot" : "panel"; - /** @member {LocusZoom.Plot} */ - this.parent_plot = this.type === "plot" ? this.parent : this.parent.parent; - - /** @member {d3.selection} */ - this.selector = null; - /** @member {LocusZoom.Dashboard.Component[]} */ - this.components = []; - /** + LocusZoom.Dashboard = function (parent) { + // parent must be a locuszoom plot or panel + if (!(parent instanceof LocusZoom.Plot) && !(parent instanceof LocusZoom.Panel)) { + throw 'Unable to create dashboard, parent must be a locuszoom plot or panel'; + } + /** @member {LocusZoom.Plot|LocusZoom.Panel} */ + this.parent = parent; + /** @member {String} */ + this.id = this.parent.getBaseId() + '.dashboard'; + /** @member {('plot'|'panel')} */ + this.type = this.parent instanceof LocusZoom.Plot ? 'plot' : 'panel'; + /** @member {LocusZoom.Plot} */ + this.parent_plot = this.type === 'plot' ? this.parent : this.parent.parent; + /** @member {d3.selection} */ + this.selector = null; + /** @member {LocusZoom.Dashboard.Component[]} */ + this.components = []; + /** * The timer identifier as returned by setTimeout * @member {Number} */ - this.hide_timeout = null; - /** + this.hide_timeout = null; + /** * Whether to hide the dashboard. Can be overridden by a child component. Check via `shouldPersist` * @protected * @member {Boolean} */ - this.persist = false; - - // TODO: Return value from constructor function? - return this.initialize(); -}; - -/** + this.persist = false; + // TODO: Return value from constructor function? + return this.initialize(); + }; + /** * Prepare the dashboard for first use: generate all component instances for this dashboard, based on the provided * layout of the parent. Connects event listeners and shows/hides as appropriate. * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.prototype.initialize = function() { - // Parse layout to generate component instances - if (Array.isArray(this.parent.layout.dashboard.components)){ - this.parent.layout.dashboard.components.forEach(function(layout){ - try { - var component = LocusZoom.Dashboard.Components.get(layout.type, layout, this); - this.components.push(component); - } catch (e) { - console.warn(e); + LocusZoom.Dashboard.prototype.initialize = function () { + // Parse layout to generate component instances + if (Array.isArray(this.parent.layout.dashboard.components)) { + this.parent.layout.dashboard.components.forEach(function (layout) { + try { + var component = LocusZoom.Dashboard.Components.get(layout.type, layout, this); + this.components.push(component); + } catch (e) { + console.warn(e); + } + }.bind(this)); } - }.bind(this)); - } - - // Add mouseover event handlers to show/hide panel dashboard - if (this.type === "panel"){ - d3.select(this.parent.parent.svg.node().parentNode).on("mouseover." + this.id, function(){ - clearTimeout(this.hide_timeout); - if (!this.selector || this.selector.style("visibility") === "hidden"){ this.show(); } - }.bind(this)); - d3.select(this.parent.parent.svg.node().parentNode).on("mouseout." + this.id, function(){ - clearTimeout(this.hide_timeout); - this.hide_timeout = setTimeout(function(){ this.hide(); }.bind(this), 300); - }.bind(this)); - } - - return this; - -}; - -/** + // Add mouseover event handlers to show/hide panel dashboard + if (this.type === 'panel') { + d3.select(this.parent.parent.svg.node().parentNode).on('mouseover.' + this.id, function () { + clearTimeout(this.hide_timeout); + if (!this.selector || this.selector.style('visibility') === 'hidden') { + this.show(); + } + }.bind(this)); + d3.select(this.parent.parent.svg.node().parentNode).on('mouseout.' + this.id, function () { + clearTimeout(this.hide_timeout); + this.hide_timeout = setTimeout(function () { + this.hide(); + }.bind(this), 300); + }.bind(this)); + } + return this; + }; + /** * Whether to persist the dashboard. Returns true if at least one component should persist, or if the panel is engaged * in an active drag event. * @returns {boolean} */ -LocusZoom.Dashboard.prototype.shouldPersist = function(){ - if (this.persist){ return true; } - var persist = false; - // Persist if at least one component should also persist - this.components.forEach(function(component){ - persist = persist || component.shouldPersist(); - }); - // Persist if in a parent drag event - persist = persist || (this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.dragging); - return !!persist; -}; - -/** + LocusZoom.Dashboard.prototype.shouldPersist = function () { + if (this.persist) { + return true; + } + var persist = false; + // Persist if at least one component should also persist + this.components.forEach(function (component) { + persist = persist || component.shouldPersist(); + }); + // Persist if in a parent drag event + persist = persist || (this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.dragging); + return !!persist; + }; + /** * Make the dashboard appear. If it doesn't exist yet create it, including creating/positioning all components within, * and make sure it is set to be visible. */ -LocusZoom.Dashboard.prototype.show = function(){ - if (!this.selector){ - switch (this.type){ - case "plot": - this.selector = d3.select(this.parent.svg.node().parentNode) - .insert("div",":first-child"); - break; - case "panel": - this.selector = d3.select(this.parent.parent.svg.node().parentNode) - .insert("div", ".lz-data_layer-tooltip, .lz-dashboard-menu, .lz-curtain").classed("lz-panel-dashboard", true); - break; - } - this.selector.classed("lz-dashboard", true).classed("lz-"+this.type+"-dashboard", true).attr("id", this.id); - } - this.components.forEach(function(component){ component.show(); }); - this.selector.style({ visibility: "visible" }); - return this.update(); -}; - -/** + LocusZoom.Dashboard.prototype.show = function () { + if (!this.selector) { + switch (this.type) { + case 'plot': + this.selector = d3.select(this.parent.svg.node().parentNode).insert('div', ':first-child'); + break; + case 'panel': + this.selector = d3.select(this.parent.parent.svg.node().parentNode).insert('div', '.lz-data_layer-tooltip, .lz-dashboard-menu, .lz-curtain').classed('lz-panel-dashboard', true); + break; + } + this.selector.classed('lz-dashboard', true).classed('lz-' + this.type + '-dashboard', true).attr('id', this.id); + } + this.components.forEach(function (component) { + component.show(); + }); + this.selector.style({ visibility: 'visible' }); + return this.update(); + }; + /** * Update the dashboard and rerender all child components. This can be called whenever plot state changes. * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.prototype.update = function(){ - if (!this.selector){ return this; } - this.components.forEach(function(component){ component.update(); }); - return this.position(); -}; - -/** + LocusZoom.Dashboard.prototype.update = function () { + if (!this.selector) { + return this; + } + this.components.forEach(function (component) { + component.update(); + }); + return this.position(); + }; + /** * Position the dashboard (and child components) within the panel * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.prototype.position = function(){ - if (!this.selector){ return this; } - // Position the dashboard itself (panel only) - if (this.type === "panel"){ - var page_origin = this.parent.getPageOrigin(); - var top = (page_origin.y + 3.5).toString() + "px"; - var left = page_origin.x.toString() + "px"; - var width = (this.parent.layout.width - 4).toString() + "px"; - this.selector.style({ position: "absolute", top: top, left: left, width: width }); - } - // Recursively position components - this.components.forEach(function(component){ component.position(); }); - return this; -}; - -/** + LocusZoom.Dashboard.prototype.position = function () { + if (!this.selector) { + return this; + } + // Position the dashboard itself (panel only) + if (this.type === 'panel') { + var page_origin = this.parent.getPageOrigin(); + var top = (page_origin.y + 3.5).toString() + 'px'; + var left = page_origin.x.toString() + 'px'; + var width = (this.parent.layout.width - 4).toString() + 'px'; + this.selector.style({ + position: 'absolute', + top: top, + left: left, + width: width + }); + } + // Recursively position components + this.components.forEach(function (component) { + component.position(); + }); + return this; + }; + /** * Hide the dashboard (make invisible but do not destroy). Will do nothing if `shouldPersist` returns true. * * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.prototype.hide = function(){ - if (!this.selector || this.shouldPersist()){ return this; } - this.components.forEach(function(component){ component.hide(); }); - this.selector.style({ visibility: "hidden" }); - return this; -}; - -/** + LocusZoom.Dashboard.prototype.hide = function () { + if (!this.selector || this.shouldPersist()) { + return this; + } + this.components.forEach(function (component) { + component.hide(); + }); + this.selector.style({ visibility: 'hidden' }); + return this; + }; + /** * Completely remove dashboard and all child components. (may be overridden by persistence settings) * @param {Boolean} [force=false] If true, will ignore persistence settings and always destroy the dashboard * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.prototype.destroy = function(force){ - if (typeof force == "undefined"){ force = false; } - if (!this.selector){ return this; } - if (this.shouldPersist() && !force){ return this; } - this.components.forEach(function(component){ component.destroy(true); }); - this.components = []; - this.selector.remove(); - this.selector = null; - return this; -}; - -/** + LocusZoom.Dashboard.prototype.destroy = function (force) { + if (typeof force == 'undefined') { + force = false; + } + if (!this.selector) { + return this; + } + if (this.shouldPersist() && !force) { + return this; + } + this.components.forEach(function (component) { + component.destroy(true); + }); + this.components = []; + this.selector.remove(); + this.selector = null; + return this; + }; + /** * * A dashboard component is an empty div rendered on a dashboard that can display custom * html of user interface elements. LocusZoom.Dashboard.Components is a singleton used to @@ -6121,806 +6119,881 @@ LocusZoom.Dashboard.prototype.destroy = function(force){ * component. Applies to buttons and menus. * @param {LocusZoom.Dashboard} parent The dashboard that contains this component */ -LocusZoom.Dashboard.Component = function(layout, parent) { - /** @member {Object} */ - this.layout = layout || {}; - if (!this.layout.color){ this.layout.color = "gray"; } - - /** @member {LocusZoom.Dashboard|*} */ - this.parent = parent || null; - /** + LocusZoom.Dashboard.Component = function (layout, parent) { + /** @member {Object} */ + this.layout = layout || {}; + if (!this.layout.color) { + this.layout.color = 'gray'; + } + /** @member {LocusZoom.Dashboard|*} */ + this.parent = parent || null; + /** * Some dashboards are attached to a panel, rather than directly to a plot * @member {LocusZoom.Panel|null} */ - this.parent_panel = null; - /** @member {LocusZoom.Plot} */ - this.parent_plot = null; - /** + this.parent_panel = null; + /** @member {LocusZoom.Plot} */ + this.parent_plot = null; + /** * This is a reference to either the panel or the plot, depending on what the dashboard is * tied to. Useful when absolutely positioning dashboard components relative to their SVG anchor. * @member {LocusZoom.Plot|LocusZoom.Panel} */ - this.parent_svg = null; - if (this.parent instanceof LocusZoom.Dashboard){ - // TODO: when is the immediate parent *not* a dashboard? - if (this.parent.type === "panel"){ - this.parent_panel = this.parent.parent; - this.parent_plot = this.parent.parent.parent; - this.parent_svg = this.parent_panel; - } else { - this.parent_plot = this.parent.parent; - this.parent_svg = this.parent_plot; - } - } - /** @member {d3.selection} */ - this.selector = null; - /** + this.parent_svg = null; + if (this.parent instanceof LocusZoom.Dashboard) { + // TODO: when is the immediate parent *not* a dashboard? + if (this.parent.type === 'panel') { + this.parent_panel = this.parent.parent; + this.parent_plot = this.parent.parent.parent; + this.parent_svg = this.parent_panel; + } else { + this.parent_plot = this.parent.parent; + this.parent_svg = this.parent_plot; + } + } + /** @member {d3.selection} */ + this.selector = null; + /** * If this is an interactive component, it will contain a button or menu instance that handles the interactivity. * There is a 1-to-1 relationship of dashboard component to button * @member {null|LocusZoom.Dashboard.Component.Button} */ - this.button = null; - /** + this.button = null; + /** * If any single component is marked persistent, it will bubble up to prevent automatic hide behavior on a * component's parent dashboard. Check via `shouldPersist` * @protected * @member {Boolean} */ - this.persist = false; - if (!this.layout.position){ this.layout.position = "left"; } - - // TODO: Return value in constructor - return this; -}; -/** + this.persist = false; + if (!this.layout.position) { + this.layout.position = 'left'; + } + // TODO: Return value in constructor + return this; + }; + /** * Perform all rendering of component, including toggling visibility to true. Will initialize and create SVG element * if necessary, as well as updating with new data and performing layout actions. */ -LocusZoom.Dashboard.Component.prototype.show = function(){ - if (!this.parent || !this.parent.selector){ return; } - if (!this.selector){ - var group_position = (["start","middle","end"].indexOf(this.layout.group_position) !== -1 ? " lz-dashboard-group-" + this.layout.group_position : ""); - this.selector = this.parent.selector.append("div") - .attr("class", "lz-dashboard-" + this.layout.position + group_position); - if (this.layout.style){ this.selector.style(this.layout.style); } - if (typeof this.initialize == "function"){ this.initialize(); } - } - if (this.button && this.button.status === "highlighted"){ this.button.menu.show(); } - this.selector.style({ visibility: "visible" }); - this.update(); - return this.position(); -}; -/** + LocusZoom.Dashboard.Component.prototype.show = function () { + if (!this.parent || !this.parent.selector) { + return; + } + if (!this.selector) { + var group_position = [ + 'start', + 'middle', + 'end' + ].indexOf(this.layout.group_position) !== -1 ? ' lz-dashboard-group-' + this.layout.group_position : ''; + this.selector = this.parent.selector.append('div').attr('class', 'lz-dashboard-' + this.layout.position + group_position); + if (this.layout.style) { + this.selector.style(this.layout.style); + } + if (typeof this.initialize == 'function') { + this.initialize(); + } + } + if (this.button && this.button.status === 'highlighted') { + this.button.menu.show(); + } + this.selector.style({ visibility: 'visible' }); + this.update(); + return this.position(); + }; + /** * Update the dashboard component with any new data or plot state as appropriate. This method performs all * necessary rendering steps. */ -LocusZoom.Dashboard.Component.prototype.update = function(){ /* stub */ }; -/** + LocusZoom.Dashboard.Component.prototype.update = function () { + }; + /** * Place the component correctly in the plot * @returns {LocusZoom.Dashboard.Component} */ -LocusZoom.Dashboard.Component.prototype.position = function(){ - if (this.button){ this.button.menu.position(); } - return this; -}; -/** + LocusZoom.Dashboard.Component.prototype.position = function () { + if (this.button) { + this.button.menu.position(); + } + return this; + }; + /** * Determine whether the component should persist (will bubble up to parent dashboard) * @returns {boolean} */ -LocusZoom.Dashboard.Component.prototype.shouldPersist = function(){ - if (this.persist){ return true; } - if (this.button && this.button.persist){ return true; } - return false; -}; -/** + LocusZoom.Dashboard.Component.prototype.shouldPersist = function () { + if (this.persist) { + return true; + } + if (this.button && this.button.persist) { + return true; + } + return false; + }; + /** * Toggle visibility to hidden, unless marked as persistent * @returns {LocusZoom.Dashboard.Component} */ -LocusZoom.Dashboard.Component.prototype.hide = function(){ - if (!this.selector || this.shouldPersist()){ return this; } - if (this.button){ this.button.menu.hide(); } - this.selector.style({ visibility: "hidden" }); - return this; -}; -/** + LocusZoom.Dashboard.Component.prototype.hide = function () { + if (!this.selector || this.shouldPersist()) { + return this; + } + if (this.button) { + this.button.menu.hide(); + } + this.selector.style({ visibility: 'hidden' }); + return this; + }; + /** * Completely remove component and button. (may be overridden by persistence settings) * @param {Boolean} [force=false] If true, will ignore persistence settings and always destroy the dashboard * @returns {LocusZoom.Dashboard} */ -LocusZoom.Dashboard.Component.prototype.destroy = function(force){ - if (typeof force == "undefined"){ force = false; } - if (!this.selector){ return this; } - if (this.shouldPersist() && !force){ return this; } - if (this.button && this.button.menu){ this.button.menu.destroy(); } - this.selector.remove(); - this.selector = null; - this.button = null; - return this; -}; - -/** + LocusZoom.Dashboard.Component.prototype.destroy = function (force) { + if (typeof force == 'undefined') { + force = false; + } + if (!this.selector) { + return this; + } + if (this.shouldPersist() && !force) { + return this; + } + if (this.button && this.button.menu) { + this.button.menu.destroy(); + } + this.selector.remove(); + this.selector = null; + this.button = null; + return this; + }; + /** * Singleton registry of all known components * @class * @static */ -LocusZoom.Dashboard.Components = (function() { - /** @lends LocusZoom.Dashboard.Components */ - var obj = {}; - var components = {}; - - /** + LocusZoom.Dashboard.Components = function () { + /** @lends LocusZoom.Dashboard.Components */ + var obj = {}; + var components = {}; + /** * Create a new component instance by name * @param {String} name The string identifier of the desired component * @param {Object} layout The layout to use to create the component * @param {LocusZoom.Dashboard} parent The containing dashboard to use when creating the component * @returns {LocusZoom.Dashboard.Component} */ - obj.get = function(name, layout, parent) { - if (!name) { - return null; - } else if (components[name]) { - if (typeof layout != "object"){ - throw("invalid layout argument for dashboard component [" + name + "]"); - } else { - return new components[name](layout, parent); - } - } else { - throw("dashboard component [" + name + "] not found"); - } - }; - /** + obj.get = function (name, layout, parent) { + if (!name) { + return null; + } else if (components[name]) { + if (typeof layout != 'object') { + throw 'invalid layout argument for dashboard component [' + name + ']'; + } else { + return new components[name](layout, parent); + } + } else { + throw 'dashboard component [' + name + '] not found'; + } + }; + /** * Add a new component constructor to the registry and ensure that it extends the correct parent class * @protected * @param name * @param component */ - obj.set = function(name, component) { - if (component) { - if (typeof component != "function"){ - throw("unable to set dashboard component [" + name + "], argument provided is not a function"); - } else { - components[name] = component; - components[name].prototype = new LocusZoom.Dashboard.Component(); - } - } else { - delete components[name]; - } - }; - - /** + obj.set = function (name, component) { + if (component) { + if (typeof component != 'function') { + throw 'unable to set dashboard component [' + name + '], argument provided is not a function'; + } else { + components[name] = component; + components[name].prototype = new LocusZoom.Dashboard.Component(); + } + } else { + delete components[name]; + } + }; + /** * Register a new component constructor by name * @param {String} name * @param {function} component The component constructor */ - obj.add = function(name, component) { - if (components[name]) { - throw("dashboard component already exists with name: " + name); - } else { - obj.set(name, component); - } - }; - - /** + obj.add = function (name, component) { + if (components[name]) { + throw 'dashboard component already exists with name: ' + name; + } else { + obj.set(name, component); + } + }; + /** * List the names of all registered components * @returns {String[]} */ - obj.list = function() { - return Object.keys(components); - }; - - return obj; -})(); - -/** + obj.list = function () { + return Object.keys(components); + }; + return obj; + }(); + /** * Plots and panels may have a "dashboard" element suited for showing HTML components that may be interactive. * When components need to incorporate a generic button, or additionally a button that generates a menu, this * class provides much of the necessary framework. * @class * @param {LocusZoom.Dashboard.Component} parent */ -LocusZoom.Dashboard.Component.Button = function(parent) { - - if (!(parent instanceof LocusZoom.Dashboard.Component)){ - throw "Unable to create dashboard component button, invalid parent"; - } - /** @member {LocusZoom.Dashboard.Component} */ - this.parent = parent; - /** @member {LocusZoom.Dashboard.Panel} */ - this.parent_panel = this.parent.parent_panel; - /** @member {LocusZoom.Dashboard.Plot} */ - this.parent_plot = this.parent.parent_plot; - /** @member {LocusZoom.Plot|LocusZoom.Panel} */ - this.parent_svg = this.parent.parent_svg; - - /** @member {LocusZoom.Dashboard|null|*} */ - this.parent_dashboard = this.parent.parent; - /** @member {d3.selection} */ - this.selector = null; - - /** + LocusZoom.Dashboard.Component.Button = function (parent) { + if (!(parent instanceof LocusZoom.Dashboard.Component)) { + throw 'Unable to create dashboard component button, invalid parent'; + } + /** @member {LocusZoom.Dashboard.Component} */ + this.parent = parent; + /** @member {LocusZoom.Dashboard.Panel} */ + this.parent_panel = this.parent.parent_panel; + /** @member {LocusZoom.Dashboard.Plot} */ + this.parent_plot = this.parent.parent_plot; + /** @member {LocusZoom.Plot|LocusZoom.Panel} */ + this.parent_svg = this.parent.parent_svg; + /** @member {LocusZoom.Dashboard|null|*} */ + this.parent_dashboard = this.parent.parent; + /** @member {d3.selection} */ + this.selector = null; + /** * Tag to use for the button (default: a) * @member {String} */ - this.tag = "a"; - - /** + this.tag = 'a'; + /** * TODO This method does not appear to be used anywhere * @param {String} tag * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setTag = function(tag){ - if (typeof tag != "undefined"){ this.tag = tag.toString(); } - return this; - }; - - /** + this.setTag = function (tag) { + if (typeof tag != 'undefined') { + this.tag = tag.toString(); + } + return this; + }; + /** * HTML for the button to show. * @protected * @member {String} */ - this.html = ""; - /** + this.html = ''; + /** * Specify the HTML content of this button. * WARNING: The string provided will be inserted into the document as raw markup; XSS mitigation is the * responsibility of each button implementation. * @param {String} html * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setHtml = function(html){ - if (typeof html != "undefined"){ this.html = html.toString(); } - return this; - }; - /** + this.setHtml = function (html) { + if (typeof html != 'undefined') { + this.html = html.toString(); + } + return this; + }; + /** * @deprecated since 0.5.6; use setHTML instead */ - this.setText = this.setHTML; - - /** + this.setText = this.setHTML; + /** * Mouseover title text for the button to show * @protected * @member {String} */ - this.title = ""; - /** + this.title = ''; + /** * Set the mouseover title text for the button (if any) * @param {String} title Simple text to display * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setTitle = function(title){ - if (typeof title != "undefined"){ this.title = title.toString(); } - return this; - }; - - /** + this.setTitle = function (title) { + if (typeof title != 'undefined') { + this.title = title.toString(); + } + return this; + }; + /** * Color of the button * @member {String} */ - this.color = "gray"; - - /** + this.color = 'gray'; + /** * Set the color associated with this button * @param {('gray'|'red'|'orange'|'yellow'|'green'|'blue'|'purple')} color Any selection not in the preset list * will be replaced with gray. * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setColor = function(color){ - if (typeof color != "undefined"){ - if (["gray", "red", "orange", "yellow", "green", "blue", "purple"].indexOf(color) !== -1){ this.color = color; } - else { this.color = "gray"; } - } - return this; - }; - - /** + this.setColor = function (color) { + if (typeof color != 'undefined') { + if ([ + 'gray', + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'purple' + ].indexOf(color) !== -1) { + this.color = color; + } else { + this.color = 'gray'; + } + } + return this; + }; + /** * Hash of arbitrary button styles to apply as {name: value} entries * @protected * @member {Object} */ - this.style = {}; - /** + this.style = {}; + /** * Set a collection of custom styles to be used by the button * @param {Object} style Hash of {name:value} entries * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setStyle = function(style){ - if (typeof style != "undefined"){ this.style = style; } - return this; - }; - - // - /** + this.setStyle = function (style) { + if (typeof style != 'undefined') { + this.style = style; + } + return this; + }; + // + /** * Method to generate a CSS class string * @returns {string} */ - this.getClass = function(){ - var group_position = (["start","middle","end"].indexOf(this.parent.layout.group_position) !== -1 ? " lz-dashboard-button-group-" + this.parent.layout.group_position : ""); - return "lz-dashboard-button lz-dashboard-button-" + this.color + (this.status ? "-" + this.status : "") + group_position; - }; - - // Permanence - /** + this.getClass = function () { + var group_position = [ + 'start', + 'middle', + 'end' + ].indexOf(this.parent.layout.group_position) !== -1 ? ' lz-dashboard-button-group-' + this.parent.layout.group_position : ''; + return 'lz-dashboard-button lz-dashboard-button-' + this.color + (this.status ? '-' + this.status : '') + group_position; + }; + // Permanence + /** * Track internal state on whether to keep showing the button/ menu contents at the moment * @protected * @member {Boolean} */ - this.persist = false; - /** + this.persist = false; + /** * Configuration when defining a button: track whether this component should be allowed to keep open * menu/button contents in response to certain events * @protected * @member {Boolean} */ - this.permanent = false; - /** + this.permanent = false; + /** * Allow code to change whether the button is allowed to be `permanent` * @param {boolean} bool * @returns {LocusZoom.Dashboard.Component.Button} */ - this.setPermanent = function(bool){ - if (typeof bool == "undefined"){ bool = true; } else { bool = Boolean(bool); } - this.permanent = bool; - if (this.permanent){ this.persist = true; } - return this; - }; - /** + this.setPermanent = function (bool) { + if (typeof bool == 'undefined') { + bool = true; + } else { + bool = Boolean(bool); + } + this.permanent = bool; + if (this.permanent) { + this.persist = true; + } + return this; + }; + /** * Determine whether the button/menu contents should persist in response to a specific event * @returns {Boolean} */ - this.shouldPersist = function(){ - return this.permanent || this.persist; - }; - - /** + this.shouldPersist = function () { + return this.permanent || this.persist; + }; + /** * Button status (highlighted / disabled/ etc) * @protected * @member {String} */ - this.status = ""; - /** + this.status = ''; + /** * Change button state * @param {('highlighted'|'disabled'|'')} status */ - this.setStatus = function(status){ - if (typeof status != "undefined" && ["", "highlighted", "disabled"].indexOf(status) !== -1){ this.status = status; } - return this.update(); - }; - /** + this.setStatus = function (status) { + if (typeof status != 'undefined' && [ + '', + 'highlighted', + 'disabled' + ].indexOf(status) !== -1) { + this.status = status; + } + return this.update(); + }; + /** * Toggle whether the button is highlighted * @param {boolean} bool If provided, explicitly set highlighted state * @returns {LocusZoom.Dashboard.Component.Button} */ - this.highlight = function(bool){ - if (typeof bool == "undefined"){ bool = true; } else { bool = Boolean(bool); } - if (bool){ return this.setStatus("highlighted"); } - else if (this.status === "highlighted"){ return this.setStatus(""); } - return this; - }; - /** + this.highlight = function (bool) { + if (typeof bool == 'undefined') { + bool = true; + } else { + bool = Boolean(bool); + } + if (bool) { + return this.setStatus('highlighted'); + } else if (this.status === 'highlighted') { + return this.setStatus(''); + } + return this; + }; + /** * Toggle whether the button is disabled * @param {boolean} bool If provided, explicitly set disabled state * @returns {LocusZoom.Dashboard.Component.Button} */ - this.disable = function(bool){ - if (typeof bool == "undefined"){ bool = true; } else { bool = Boolean(bool); } - if (bool){ return this.setStatus("disabled"); } - else if (this.status === "disabled"){ return this.setStatus(""); } - return this; - }; - - // Mouse events - /** @member {function} */ - this.onmouseover = function(){}; - this.setOnMouseover = function(onmouseover){ - if (typeof onmouseover == "function"){ this.onmouseover = onmouseover; } - else { this.onmouseover = function(){}; } - return this; - }; - /** @member {function} */ - this.onmouseout = function(){}; - this.setOnMouseout = function(onmouseout){ - if (typeof onmouseout == "function"){ this.onmouseout = onmouseout; } - else { this.onmouseout = function(){}; } - return this; - }; - /** @member {function} */ - this.onclick = function(){}; - this.setOnclick = function(onclick){ - if (typeof onclick == "function"){ this.onclick = onclick; } - else { this.onclick = function(){}; } - return this; - }; - - // Primary behavior functions - /** + this.disable = function (bool) { + if (typeof bool == 'undefined') { + bool = true; + } else { + bool = Boolean(bool); + } + if (bool) { + return this.setStatus('disabled'); + } else if (this.status === 'disabled') { + return this.setStatus(''); + } + return this; + }; + // Mouse events + /** @member {function} */ + this.onmouseover = function () { + }; + this.setOnMouseover = function (onmouseover) { + if (typeof onmouseover == 'function') { + this.onmouseover = onmouseover; + } else { + this.onmouseover = function () { + }; + } + return this; + }; + /** @member {function} */ + this.onmouseout = function () { + }; + this.setOnMouseout = function (onmouseout) { + if (typeof onmouseout == 'function') { + this.onmouseout = onmouseout; + } else { + this.onmouseout = function () { + }; + } + return this; + }; + /** @member {function} */ + this.onclick = function () { + }; + this.setOnclick = function (onclick) { + if (typeof onclick == 'function') { + this.onclick = onclick; + } else { + this.onclick = function () { + }; + } + return this; + }; + // Primary behavior functions + /** * Show the button, including creating DOM elements if necessary for first render */ - this.show = function(){ - if (!this.parent){ return; } - if (!this.selector){ - this.selector = this.parent.selector.append(this.tag).attr("class", this.getClass()); - } - return this.update(); - }; - /** + this.show = function () { + if (!this.parent) { + return; + } + if (!this.selector) { + this.selector = this.parent.selector.append(this.tag).attr('class', this.getClass()); + } + return this.update(); + }; + /** * Hook for any actions or state cleanup to be performed before rerendering * @returns {LocusZoom.Dashboard.Component.Button} */ - this.preUpdate = function(){ return this; }; - /** + this.preUpdate = function () { + return this; + }; + /** * Update button state and contents, and fully rerender * @returns {LocusZoom.Dashboard.Component.Button} */ - this.update = function(){ - if (!this.selector){ return this; } - this.preUpdate(); - this.selector - .attr("class", this.getClass()) - .attr("title", this.title).style(this.style) - .on("mouseover", (this.status === "disabled") ? null : this.onmouseover) - .on("mouseout", (this.status === "disabled") ? null : this.onmouseout) - .on("click", (this.status === "disabled") ? null : this.onclick) - .html(this.html); - this.menu.update(); - this.postUpdate(); - return this; - }; - /** + this.update = function () { + if (!this.selector) { + return this; + } + this.preUpdate(); + this.selector.attr('class', this.getClass()).attr('title', this.title).style(this.style).on('mouseover', this.status === 'disabled' ? null : this.onmouseover).on('mouseout', this.status === 'disabled' ? null : this.onmouseout).on('click', this.status === 'disabled' ? null : this.onclick).html(this.html); + this.menu.update(); + this.postUpdate(); + return this; + }; + /** * Hook for any behavior to be added/changed after the button has been re-rendered * @returns {LocusZoom.Dashboard.Component.Button} */ - this.postUpdate = function(){ return this; }; - /** + this.postUpdate = function () { + return this; + }; + /** * Hide the button by removing it from the DOM (may be overridden by current persistence setting) * @returns {LocusZoom.Dashboard.Component.Button} */ - this.hide = function(){ - if (this.selector && !this.shouldPersist()){ - this.selector.remove(); - this.selector = null; - } - return this; - }; - - /** + this.hide = function () { + if (this.selector && !this.shouldPersist()) { + this.selector.remove(); + this.selector = null; + } + return this; + }; + /** * Button Menu Object * The menu is an HTML overlay that can appear below a button. It can contain arbitrary HTML and * has logic to be automatically positioned and sized to behave more or less like a dropdown menu. * @member {Object} */ - this.menu = { - outer_selector: null, - inner_selector: null, - scroll_position: 0, - hidden: true, - /** + this.menu = { + outer_selector: null, + inner_selector: null, + scroll_position: 0, + hidden: true, + /** * Show the button menu, including setting up any DOM elements needed for first rendering */ - show: function(){ - if (!this.menu.outer_selector){ - this.menu.outer_selector = d3.select(this.parent_plot.svg.node().parentNode).append("div") - .attr("class", "lz-dashboard-menu lz-dashboard-menu-" + this.color) - .attr("id", this.parent_svg.getBaseId() + ".dashboard.menu"); - this.menu.inner_selector = this.menu.outer_selector.append("div") - .attr("class", "lz-dashboard-menu-content"); - this.menu.inner_selector.on("scroll", function(){ - this.menu.scroll_position = this.menu.inner_selector.node().scrollTop; - }.bind(this)); - } - this.menu.outer_selector.style({ visibility: "visible" }); - this.menu.hidden = false; - return this.menu.update(); - }.bind(this), - /** + show: function () { + if (!this.menu.outer_selector) { + this.menu.outer_selector = d3.select(this.parent_plot.svg.node().parentNode).append('div').attr('class', 'lz-dashboard-menu lz-dashboard-menu-' + this.color).attr('id', this.parent_svg.getBaseId() + '.dashboard.menu'); + this.menu.inner_selector = this.menu.outer_selector.append('div').attr('class', 'lz-dashboard-menu-content'); + this.menu.inner_selector.on('scroll', function () { + this.menu.scroll_position = this.menu.inner_selector.node().scrollTop; + }.bind(this)); + } + this.menu.outer_selector.style({ visibility: 'visible' }); + this.menu.hidden = false; + return this.menu.update(); + }.bind(this), + /** * Update the rendering of the menu */ - update: function(){ - if (!this.menu.outer_selector){ return this.menu; } - this.menu.populate(); // This function is stubbed for all buttons by default and custom implemented in component definition - if (this.menu.inner_selector){ this.menu.inner_selector.node().scrollTop = this.menu.scroll_position; } - return this.menu.position(); - }.bind(this), - position: function(){ - if (!this.menu.outer_selector){ return this.menu; } - // Unset any explicitly defined outer selector height so that menus dynamically shrink if content is removed - this.menu.outer_selector.style({ height: null }); - var padding = 3; - var scrollbar_padding = 20; - var menu_height_padding = 14; // 14: 2x 6px padding, 2x 1px border - var page_origin = this.parent_svg.getPageOrigin(); - var page_scroll_top = document.documentElement.scrollTop || document.body.scrollTop; - var container_offset = this.parent_plot.getContainerOffset(); - var dashboard_client_rect = this.parent_dashboard.selector.node().getBoundingClientRect(); - var button_client_rect = this.selector.node().getBoundingClientRect(); - var menu_client_rect = this.menu.outer_selector.node().getBoundingClientRect(); - var total_content_height = this.menu.inner_selector.node().scrollHeight; - var top = 0; var left = 0; - if (this.parent_dashboard.type === "panel"){ - top = (page_origin.y + dashboard_client_rect.height + (2 * padding)); - left = Math.max(page_origin.x + this.parent_svg.layout.width - menu_client_rect.width - padding, page_origin.x + padding); - } else { - top = button_client_rect.bottom + page_scroll_top + padding - container_offset.top; - left = Math.max(button_client_rect.left + button_client_rect.width - menu_client_rect.width - container_offset.left, page_origin.x + padding); - } - var base_max_width = Math.max(this.parent_svg.layout.width - (2 * padding) - scrollbar_padding, scrollbar_padding); - var container_max_width = base_max_width; - var content_max_width = (base_max_width - (4 * padding)); - var base_max_height = Math.max(this.parent_svg.layout.height - (10 * padding) - menu_height_padding, menu_height_padding); - var height = Math.min(total_content_height, base_max_height); - var max_height = base_max_height; - this.menu.outer_selector.style({ - "top": top.toString() + "px", - "left": left.toString() + "px", - "max-width": container_max_width.toString() + "px", - "max-height": max_height.toString() + "px", - "height": height.toString() + "px" - }); - this.menu.inner_selector.style({ "max-width": content_max_width.toString() + "px" }); - this.menu.inner_selector.node().scrollTop = this.menu.scroll_position; - return this.menu; - }.bind(this), - hide: function(){ - if (!this.menu.outer_selector){ return this.menu; } - this.menu.outer_selector.style({ visibility: "hidden" }); - this.menu.hidden = true; - return this.menu; - }.bind(this), - destroy: function(){ - if (!this.menu.outer_selector){ return this.menu; } - this.menu.inner_selector.remove(); - this.menu.outer_selector.remove(); - this.menu.inner_selector = null; - this.menu.outer_selector = null; - return this.menu; - }.bind(this), - /** + update: function () { + if (!this.menu.outer_selector) { + return this.menu; + } + this.menu.populate(); + // This function is stubbed for all buttons by default and custom implemented in component definition + if (this.menu.inner_selector) { + this.menu.inner_selector.node().scrollTop = this.menu.scroll_position; + } + return this.menu.position(); + }.bind(this), + position: function () { + if (!this.menu.outer_selector) { + return this.menu; + } + // Unset any explicitly defined outer selector height so that menus dynamically shrink if content is removed + this.menu.outer_selector.style({ height: null }); + var padding = 3; + var scrollbar_padding = 20; + var menu_height_padding = 14; + // 14: 2x 6px padding, 2x 1px border + var page_origin = this.parent_svg.getPageOrigin(); + var page_scroll_top = document.documentElement.scrollTop || document.body.scrollTop; + var container_offset = this.parent_plot.getContainerOffset(); + var dashboard_client_rect = this.parent_dashboard.selector.node().getBoundingClientRect(); + var button_client_rect = this.selector.node().getBoundingClientRect(); + var menu_client_rect = this.menu.outer_selector.node().getBoundingClientRect(); + var total_content_height = this.menu.inner_selector.node().scrollHeight; + var top = 0; + var left = 0; + if (this.parent_dashboard.type === 'panel') { + top = page_origin.y + dashboard_client_rect.height + 2 * padding; + left = Math.max(page_origin.x + this.parent_svg.layout.width - menu_client_rect.width - padding, page_origin.x + padding); + } else { + top = button_client_rect.bottom + page_scroll_top + padding - container_offset.top; + left = Math.max(button_client_rect.left + button_client_rect.width - menu_client_rect.width - container_offset.left, page_origin.x + padding); + } + var base_max_width = Math.max(this.parent_svg.layout.width - 2 * padding - scrollbar_padding, scrollbar_padding); + var container_max_width = base_max_width; + var content_max_width = base_max_width - 4 * padding; + var base_max_height = Math.max(this.parent_svg.layout.height - 10 * padding - menu_height_padding, menu_height_padding); + var height = Math.min(total_content_height, base_max_height); + var max_height = base_max_height; + this.menu.outer_selector.style({ + 'top': top.toString() + 'px', + 'left': left.toString() + 'px', + 'max-width': container_max_width.toString() + 'px', + 'max-height': max_height.toString() + 'px', + 'height': height.toString() + 'px' + }); + this.menu.inner_selector.style({ 'max-width': content_max_width.toString() + 'px' }); + this.menu.inner_selector.node().scrollTop = this.menu.scroll_position; + return this.menu; + }.bind(this), + hide: function () { + if (!this.menu.outer_selector) { + return this.menu; + } + this.menu.outer_selector.style({ visibility: 'hidden' }); + this.menu.hidden = true; + return this.menu; + }.bind(this), + destroy: function () { + if (!this.menu.outer_selector) { + return this.menu; + } + this.menu.inner_selector.remove(); + this.menu.outer_selector.remove(); + this.menu.inner_selector = null; + this.menu.outer_selector = null; + return this.menu; + }.bind(this), + /** * Internal method definition * By convention populate() does nothing and should be reimplemented with each dashboard button definition * Reimplement by way of Dashboard.Component.Button.menu.setPopulate to define the populate method and hook * up standard menu click-toggle behavior prototype. * @protected */ - populate: function(){ /* stub */ }.bind(this), - /** + populate: function () { + }.bind(this), + /** * Define how the menu is populated with items, and set up click and display properties as appropriate * @public */ - setPopulate: function(menu_populate_function){ - if (typeof menu_populate_function == "function"){ - this.menu.populate = menu_populate_function; - this.setOnclick(function(){ - if (this.menu.hidden){ - this.menu.show(); - this.highlight().update(); - this.persist = true; + setPopulate: function (menu_populate_function) { + if (typeof menu_populate_function == 'function') { + this.menu.populate = menu_populate_function; + this.setOnclick(function () { + if (this.menu.hidden) { + this.menu.show(); + this.highlight().update(); + this.persist = true; + } else { + this.menu.hide(); + this.highlight(false).update(); + if (!this.permanent) { + this.persist = false; + } + } + }.bind(this)); } else { - this.menu.hide(); - this.highlight(false).update(); - if (!this.permanent){ this.persist = false; } + this.setOnclick(); } - }.bind(this)); - } else { - this.setOnclick(); - } - return this; - }.bind(this) - }; - -}; - -/** + return this; + }.bind(this) + }; + }; + /** * Renders arbitrary text with title formatting * @class LocusZoom.Dashboard.Components.title * @augments LocusZoom.Dashboard.Component * @param {object} layout * @param {string} layout.title Text to render */ -LocusZoom.Dashboard.Components.add("title", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.show = function(){ - this.div_selector = this.parent.selector.append("div") - .attr("class", "lz-dashboard-title lz-dashboard-" + this.layout.position); - this.title_selector = this.div_selector.append("h3"); - return this.update(); - }; - this.update = function(){ - var title = layout.title.toString(); - if (this.layout.subtitle){ title += " " + this.layout.subtitle + ""; } - this.title_selector.html(title); - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('title', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.show = function () { + this.div_selector = this.parent.selector.append('div').attr('class', 'lz-dashboard-title lz-dashboard-' + this.layout.position); + this.title_selector = this.div_selector.append('h3'); + return this.update(); + }; + this.update = function () { + var title = layout.title.toString(); + if (this.layout.subtitle) { + title += ' ' + this.layout.subtitle + ''; + } + this.title_selector.html(title); + return this; + }; + }); + /** * Renders text to display the current dimensions of the plot. Automatically updated as plot dimensions change * @class LocusZoom.Dashboard.Components.dimensions * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("dimensions", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - var display_width = this.parent_plot.layout.width.toString().indexOf(".") === -1 ? this.parent_plot.layout.width : this.parent_plot.layout.width.toFixed(2); - var display_height = this.parent_plot.layout.height.toString().indexOf(".") === -1 ? this.parent_plot.layout.height : this.parent_plot.layout.height.toFixed(2); - this.selector.html(display_width + "px × " + display_height + "px"); - if (layout.class){ this.selector.attr("class", layout.class); } - if (layout.style){ this.selector.style(layout.style); } - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('dimensions', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + var display_width = this.parent_plot.layout.width.toString().indexOf('.') === -1 ? this.parent_plot.layout.width : this.parent_plot.layout.width.toFixed(2); + var display_height = this.parent_plot.layout.height.toString().indexOf('.') === -1 ? this.parent_plot.layout.height : this.parent_plot.layout.height.toFixed(2); + this.selector.html(display_width + 'px \xD7 ' + display_height + 'px'); + if (layout.class) { + this.selector.attr('class', layout.class); + } + if (layout.style) { + this.selector.style(layout.style); + } + return this; + }; + }); + /** * Display the current scale of the genome region displayed in the plot, as defined by the difference between * `state.end` and `state.start`. * @class LocusZoom.Dashboard.Components.region_scale * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("region_scale", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (!isNaN(this.parent_plot.state.start) && !isNaN(this.parent_plot.state.end) - && this.parent_plot.state.start !== null && this.parent_plot.state.end !== null){ - this.selector.style("display", null); - this.selector.html(LocusZoom.positionIntToString(this.parent_plot.state.end - this.parent_plot.state.start, null, true)); - } else { - this.selector.style("display", "none"); - } - if (layout.class){ this.selector.attr("class", layout.class); } - if (layout.style){ this.selector.style(layout.style); } - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('region_scale', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (!isNaN(this.parent_plot.state.start) && !isNaN(this.parent_plot.state.end) && this.parent_plot.state.start !== null && this.parent_plot.state.end !== null) { + this.selector.style('display', null); + this.selector.html(LocusZoom.positionIntToString(this.parent_plot.state.end - this.parent_plot.state.start, null, true)); + } else { + this.selector.style('display', 'none'); + } + if (layout.class) { + this.selector.attr('class', layout.class); + } + if (layout.style) { + this.selector.style(layout.style); + } + return this; + }; + }); + /** * Button to export current plot to an SVG image * @class LocusZoom.Dashboard.Components.download * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("download", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (this.button){ return this; } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml("Download Image").setTitle("Download image of the current plot as locuszoom.svg") - .setOnMouseover(function() { - this.button.selector - .classed("lz-dashboard-button-gray-disabled", true) - .html("Preparing Image"); - this.generateBase64SVG().then(function(base64_string){ - this.button.selector - .attr("href", "data:image/svg+xml;base64,\n" + base64_string) - .classed("lz-dashboard-button-gray-disabled", false) - .classed("lz-dashboard-button-gray-highlighted", true) - .html("Download Image"); + LocusZoom.Dashboard.Components.add('download', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml('Download Image').setTitle('Download image of the current plot as locuszoom.svg').setOnMouseover(function () { + this.button.selector.classed('lz-dashboard-button-gray-disabled', true).html('Preparing Image'); + this.generateBase64SVG().then(function (base64_string) { + this.button.selector.attr('href', 'data:image/svg+xml;base64,\n' + base64_string).classed('lz-dashboard-button-gray-disabled', false).classed('lz-dashboard-button-gray-highlighted', true).html('Download Image'); + }.bind(this)); + }.bind(this)).setOnMouseout(function () { + this.button.selector.classed('lz-dashboard-button-gray-highlighted', false); }.bind(this)); - }.bind(this)) - .setOnMouseout(function() { - this.button.selector.classed("lz-dashboard-button-gray-highlighted", false); - }.bind(this)); - this.button.show(); - this.button.selector.attr("href-lang", "image/svg+xml").attr("download", "locuszoom.svg"); - return this; - }; - this.css_string = ""; - for (var stylesheet in Object.keys(document.styleSheets)){ - if ( document.styleSheets[stylesheet].href !== null - && document.styleSheets[stylesheet].href.indexOf("locuszoom.css") !== -1){ - LocusZoom.createCORSPromise("GET", document.styleSheets[stylesheet].href) - .then(function(response){ - this.css_string = response.replace(/[\r\n]/g," ").replace(/\s+/g," "); - if (this.css_string.indexOf("/* ! LocusZoom HTML Styles */")){ - this.css_string = this.css_string.substring(0, this.css_string.indexOf("/* ! LocusZoom HTML Styles */")); - } + this.button.show(); + this.button.selector.attr('href-lang', 'image/svg+xml').attr('download', 'locuszoom.svg'); + return this; + }; + this.css_string = ''; + for (var stylesheet in Object.keys(document.styleSheets)) { + if (document.styleSheets[stylesheet].href !== null && document.styleSheets[stylesheet].href.indexOf('locuszoom.css') !== -1) { + LocusZoom.createCORSPromise('GET', document.styleSheets[stylesheet].href).then(function (response) { + this.css_string = response.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' '); + if (this.css_string.indexOf('/* ! LocusZoom HTML Styles */')) { + this.css_string = this.css_string.substring(0, this.css_string.indexOf('/* ! LocusZoom HTML Styles */')); + } + }.bind(this)); + break; + } + } + this.generateBase64SVG = function () { + return Q.fcall(function () { + // Insert a hidden div, clone the node into that so we can modify it with d3 + var container = this.parent.selector.append('div').style('display', 'none').html(this.parent_plot.svg.node().outerHTML); + // Remove unnecessary elements + container.selectAll('g.lz-curtain').remove(); + container.selectAll('g.lz-mouse_guide').remove(); + // Convert units on axis tick dy attributes from ems to pixels + container.selectAll('g.tick text').each(function () { + var dy = +d3.select(this).attr('dy').substring(-2).slice(0, -2) * 10; + d3.select(this).attr('dy', dy); + }); + // Pull the svg into a string and add the contents of the locuszoom stylesheet + // Don't add this with d3 because it will escape the CDATA declaration incorrectly + var initial_html = d3.select(container.select('svg').node().parentNode).html(); + var style_def = ''; + var insert_at = initial_html.indexOf('>') + 1; + initial_html = initial_html.slice(0, insert_at) + style_def + initial_html.slice(insert_at); + // Delete the container node + container.remove(); + // Base64-encode the string and return it + return btoa(encodeURIComponent(initial_html).replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode('0x' + p1); + })); }.bind(this)); - break; - } - } - this.generateBase64SVG = function(){ - return Q.fcall(function () { - // Insert a hidden div, clone the node into that so we can modify it with d3 - var container = this.parent.selector.append("div").style("display", "none") - .html(this.parent_plot.svg.node().outerHTML); - // Remove unnecessary elements - container.selectAll("g.lz-curtain").remove(); - container.selectAll("g.lz-mouse_guide").remove(); - // Convert units on axis tick dy attributes from ems to pixels - container.selectAll("g.tick text").each(function(){ - var dy = +(d3.select(this).attr("dy").substring(-2).slice(0,-2))*10; - d3.select(this).attr("dy", dy); - }); - // Pull the svg into a string and add the contents of the locuszoom stylesheet - // Don't add this with d3 because it will escape the CDATA declaration incorrectly - var initial_html = d3.select(container.select("svg").node().parentNode).html(); - var style_def = ""; - var insert_at = initial_html.indexOf(">") + 1; - initial_html = initial_html.slice(0,insert_at) + style_def + initial_html.slice(insert_at); - // Delete the container node - container.remove(); - // Base64-encode the string and return it - return btoa(encodeURIComponent(initial_html).replace(/%([0-9A-F]{2})/g, function(match, p1) { - return String.fromCharCode("0x" + p1); - })); - }.bind(this)); - }; -}); - -/** + }; + }); + /** * Button to remove panel from plot. * NOTE: Will only work on panel dashboards. * @class LocusZoom.Dashboard.Components.remove_panel * @augments LocusZoom.Dashboard.Component * @param {Boolean} [layout.suppress_confirm=false] If true, removes the panel without prompting user for confirmation */ -LocusZoom.Dashboard.Components.add("remove_panel", function(layout) { - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function() { - if (this.button){ return this; } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml("×").setTitle("Remove panel") - .setOnclick(function(){ - if (!layout.suppress_confirm && !confirm("Are you sure you want to remove this panel? This cannot be undone!")){ - return false; + LocusZoom.Dashboard.Components.add('remove_panel', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + return this; } - var panel = this.parent_panel; - panel.dashboard.hide(true); - d3.select(panel.parent.svg.node().parentNode).on("mouseover." + panel.getBaseId() + ".dashboard", null); - d3.select(panel.parent.svg.node().parentNode).on("mouseout." + panel.getBaseId() + ".dashboard", null); - return panel.parent.removePanel(panel.id); - }.bind(this)); - this.button.show(); - return this; - }; -}); - -/** + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml('\xD7').setTitle('Remove panel').setOnclick(function () { + if (!layout.suppress_confirm && !confirm('Are you sure you want to remove this panel? This cannot be undone!')) { + return false; + } + var panel = this.parent_panel; + panel.dashboard.hide(true); + d3.select(panel.parent.svg.node().parentNode).on('mouseover.' + panel.getBaseId() + '.dashboard', null); + d3.select(panel.parent.svg.node().parentNode).on('mouseout.' + panel.getBaseId() + '.dashboard', null); + return panel.parent.removePanel(panel.id); + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Button to move panel up relative to other panels (in terms of y-index on the page) * NOTE: Will only work on panel dashboards. * @class LocusZoom.Dashboard.Components.move_panel_up * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("move_panel_up", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (this.button){ - var is_at_top = (this.parent_panel.layout.y_index === 0); - this.button.disable(is_at_top); - return this; - } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml("▴").setTitle("Move panel up") - .setOnclick(function(){ - this.parent_panel.moveUp(); - this.update(); - }.bind(this)); - this.button.show(); - return this.update(); - }; -}); - -/** + LocusZoom.Dashboard.Components.add('move_panel_up', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + var is_at_top = this.parent_panel.layout.y_index === 0; + this.button.disable(is_at_top); + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml('\u25B4').setTitle('Move panel up').setOnclick(function () { + this.parent_panel.moveUp(); + this.update(); + }.bind(this)); + this.button.show(); + return this.update(); + }; + }); + /** * Button to move panel down relative to other panels (in terms of y-index on the page) * NOTE: Will only work on panel dashboards. * @class LocusZoom.Dashboard.Components.move_panel_down * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("move_panel_down", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (this.button){ - var is_at_bottom = (this.parent_panel.layout.y_index === this.parent_plot.panel_ids_by_y_index.length-1); - this.button.disable(is_at_bottom); - return this; - } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml("▾").setTitle("Move panel down") - .setOnclick(function(){ - this.parent_panel.moveDown(); - this.update(); - }.bind(this)); - this.button.show(); - return this.update(); - }; -}); - -/** + LocusZoom.Dashboard.Components.add('move_panel_down', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + var is_at_bottom = this.parent_panel.layout.y_index === this.parent_plot.panel_ids_by_y_index.length - 1; + this.button.disable(is_at_bottom); + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml('\u25BE').setTitle('Move panel down').setOnclick(function () { + this.parent_panel.moveDown(); + this.update(); + }.bind(this)); + this.button.show(); + return this.update(); + }; + }); + /** * Button to shift plot region forwards or back by a `step` increment provided in the layout * @class LocusZoom.Dashboard.Components.shift_region * @augments LocusZoom.Dashboard.Component @@ -6929,89 +7002,95 @@ LocusZoom.Dashboard.Components.add("move_panel_down", function(layout){ * @param {string} [layout.button_html] * @param {string} [layout.button_title] */ -LocusZoom.Dashboard.Components.add("shift_region", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)){ - this.update = function(){}; - console.warn("Unable to add shift_region dashboard component: plot state does not have region bounds"); - return; - } - if (isNaN(layout.step) || layout.step === 0){ layout.step = 50000; } - if (typeof layout.button_html !== "string"){ layout.button_html = layout.step > 0 ? ">" : "<"; } - if (typeof layout.button_title !== "string"){ - layout.button_title = "Shift region by " + (layout.step > 0 ? "+" : "-") + LocusZoom.positionIntToString(Math.abs(layout.step),null,true); - } - this.update = function(){ - if (this.button){ return this; } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title) - .setOnclick(function(){ - this.parent_plot.applyState({ - start: Math.max(this.parent_plot.state.start + layout.step, 1), - end: this.parent_plot.state.end + layout.step - }); - }.bind(this)); - this.button.show(); - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('shift_region', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)) { + this.update = function () { + }; + console.warn('Unable to add shift_region dashboard component: plot state does not have region bounds'); + return; + } + if (isNaN(layout.step) || layout.step === 0) { + layout.step = 50000; + } + if (typeof layout.button_html !== 'string') { + layout.button_html = layout.step > 0 ? '>' : '<'; + } + if (typeof layout.button_title !== 'string') { + layout.button_title = 'Shift region by ' + (layout.step > 0 ? '+' : '-') + LocusZoom.positionIntToString(Math.abs(layout.step), null, true); + } + this.update = function () { + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title).setOnclick(function () { + this.parent_plot.applyState({ + start: Math.max(this.parent_plot.state.start + layout.step, 1), + end: this.parent_plot.state.end + layout.step + }); + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Zoom in or out on the plot, centered on the middle of the plot region, by the specified amount * @class LocusZoom.Dashboard.Components.zoom_region * @augments LocusZoom.Dashboard.Component * @param {object} layout * @param {number} [layout.step=0.2] The amount to zoom in by (where 1 indicates 100%) */ -LocusZoom.Dashboard.Components.add("zoom_region", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)){ - this.update = function(){}; - console.warn("Unable to add zoom_region dashboard component: plot state does not have region bounds"); - return; - } - if (isNaN(layout.step) || layout.step === 0){ layout.step = 0.2; } - if (typeof layout.button_html != "string"){ layout.button_html = layout.step > 0 ? "z–" : "z+"; } - if (typeof layout.button_title != "string"){ - layout.button_title = "Zoom region " + (layout.step > 0 ? "out" : "in") + " by " + (Math.abs(layout.step)*100).toFixed(1) + "%"; - } - this.update = function(){ - if (this.button){ - var can_zoom = true; - var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start; - if (layout.step > 0 && !isNaN(this.parent_plot.layout.max_region_scale) && current_region_scale >= this.parent_plot.layout.max_region_scale){ - can_zoom = false; + LocusZoom.Dashboard.Components.add('zoom_region', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)) { + this.update = function () { + }; + console.warn('Unable to add zoom_region dashboard component: plot state does not have region bounds'); + return; } - if (layout.step < 0 && !isNaN(this.parent_plot.layout.min_region_scale) && current_region_scale <= this.parent_plot.layout.min_region_scale){ - can_zoom = false; + if (isNaN(layout.step) || layout.step === 0) { + layout.step = 0.2; } - this.button.disable(!can_zoom); - return this; - } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title) - .setOnclick(function(){ - var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start; - var zoom_factor = 1 + layout.step; - var new_region_scale = current_region_scale * zoom_factor; - if (!isNaN(this.parent_plot.layout.max_region_scale)){ - new_region_scale = Math.min(new_region_scale, this.parent_plot.layout.max_region_scale); - } - if (!isNaN(this.parent_plot.layout.min_region_scale)){ - new_region_scale = Math.max(new_region_scale, this.parent_plot.layout.min_region_scale); - } - var delta = Math.floor((new_region_scale - current_region_scale) / 2); - this.parent_plot.applyState({ - start: Math.max(this.parent_plot.state.start - delta, 1), - end: this.parent_plot.state.end + delta - }); - }.bind(this)); - this.button.show(); - return this; - }; -}); - -/** + if (typeof layout.button_html != 'string') { + layout.button_html = layout.step > 0 ? 'z\u2013' : 'z+'; + } + if (typeof layout.button_title != 'string') { + layout.button_title = 'Zoom region ' + (layout.step > 0 ? 'out' : 'in') + ' by ' + (Math.abs(layout.step) * 100).toFixed(1) + '%'; + } + this.update = function () { + if (this.button) { + var can_zoom = true; + var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start; + if (layout.step > 0 && !isNaN(this.parent_plot.layout.max_region_scale) && current_region_scale >= this.parent_plot.layout.max_region_scale) { + can_zoom = false; + } + if (layout.step < 0 && !isNaN(this.parent_plot.layout.min_region_scale) && current_region_scale <= this.parent_plot.layout.min_region_scale) { + can_zoom = false; + } + this.button.disable(!can_zoom); + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title).setOnclick(function () { + var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start; + var zoom_factor = 1 + layout.step; + var new_region_scale = current_region_scale * zoom_factor; + if (!isNaN(this.parent_plot.layout.max_region_scale)) { + new_region_scale = Math.min(new_region_scale, this.parent_plot.layout.max_region_scale); + } + if (!isNaN(this.parent_plot.layout.min_region_scale)) { + new_region_scale = Math.max(new_region_scale, this.parent_plot.layout.min_region_scale); + } + var delta = Math.floor((new_region_scale - current_region_scale) / 2); + this.parent_plot.applyState({ + start: Math.max(this.parent_plot.state.start - delta, 1), + end: this.parent_plot.state.end + delta + }); + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Renders button with arbitrary text that, when clicked, shows a dropdown containing arbitrary HTML * NOTE: Trusts content exactly as given. XSS prevention is the responsibility of the implementer. * @class LocusZoom.Dashboard.Components.menu @@ -7021,21 +7100,21 @@ LocusZoom.Dashboard.Components.add("zoom_region", function(layout){ * @param {string} layout.button_title Text to display as a tooltip when hovering over the button * @param {string} layout.menu_html The HTML content of the dropdown menu */ -LocusZoom.Dashboard.Components.add("menu", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (this.button){ return this; } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title); - this.button.menu.setPopulate(function(){ - this.button.menu.inner_selector.html(layout.menu_html); - }.bind(this)); - this.button.show(); - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('menu', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title); + this.button.menu.setPopulate(function () { + this.button.menu.inner_selector.html(layout.menu_html); + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Special button/menu to allow model building by tracking individual covariants. Will track a list of covariate * objects and store them in the special `model.covariates` field of plot `state`. * @class LocusZoom.Dashboard.Components.covariates_model @@ -7044,311 +7123,278 @@ LocusZoom.Dashboard.Components.add("menu", function(layout){ * @param {string} layout.button_html The HTML to render inside the button * @param {string} layout.button_title Text to display as a tooltip when hovering over the button */ -LocusZoom.Dashboard.Components.add("covariates_model", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - - this.initialize = function(){ - // Initialize state.model.covariates - this.parent_plot.state.model = this.parent_plot.state.model || {}; - this.parent_plot.state.model.covariates = this.parent_plot.state.model.covariates || []; - // Create an object at the plot level for easy access to interface methods in custom client-side JS - /** + LocusZoom.Dashboard.Components.add('covariates_model', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.initialize = function () { + // Initialize state.model.covariates + this.parent_plot.state.model = this.parent_plot.state.model || {}; + this.parent_plot.state.model.covariates = this.parent_plot.state.model.covariates || []; + // Create an object at the plot level for easy access to interface methods in custom client-side JS + /** * When a covariates model dashboard element is present, create (one) object at the plot level that exposes * component data and state for custom interactions with other plot elements. * @class LocusZoom.Plot.CovariatesModel */ - this.parent_plot.CovariatesModel = { - /** @member {LocusZoom.Dashboard.Component.Button} */ - button: this, - /** + this.parent_plot.CovariatesModel = { + /** @member {LocusZoom.Dashboard.Component.Button} */ + button: this, + /** * Add an element to the model and show a representation of it in the dashboard component menu. If the * element is already part of the model, do nothing (to avoid adding duplicates). * When plot state is changed, this will automatically trigger requests for new data accordingly. * @param {string|object} element_reference Can be any value that can be put through JSON.stringify() * to create a serialized representation of itself. */ - add: function(element_reference){ - var element = JSON.parse(JSON.stringify(element_reference)); - if (typeof element_reference == "object" && typeof element.html != "string"){ - element.html = ( (typeof element_reference.toHTML == "function") ? element_reference.toHTML() : element_reference.toString()); - } - // Check if the element is already in the model covariates array and return if it is. - for (var i = 0; i < this.state.model.covariates.length; i++) { - if (JSON.stringify(this.state.model.covariates[i]) === JSON.stringify(element)) { + add: function (element_reference) { + var element = JSON.parse(JSON.stringify(element_reference)); + if (typeof element_reference == 'object' && typeof element.html != 'string') { + element.html = typeof element_reference.toHTML == 'function' ? element_reference.toHTML() : element_reference.toString(); + } + // Check if the element is already in the model covariates array and return if it is. + for (var i = 0; i < this.state.model.covariates.length; i++) { + if (JSON.stringify(this.state.model.covariates[i]) === JSON.stringify(element)) { + return this; + } + } + this.state.model.covariates.push(element); + this.applyState(); + this.CovariatesModel.updateComponent(); return this; - } - } - this.state.model.covariates.push(element); - this.applyState(); - this.CovariatesModel.updateComponent(); - return this; - }.bind(this.parent_plot), - /** + }.bind(this.parent_plot), + /** * Remove an element from `state.model.covariates` (and from the dashboard component menu's * representation of the state model). When plot state is changed, this will automatically trigger * requests for new data accordingly. * @param {number} idx Array index of the element, in the `state.model.covariates array`. */ - removeByIdx: function(idx){ - if (typeof this.state.model.covariates[idx] == "undefined"){ - throw("Unable to remove model covariate, invalid index: " + idx.toString()); - } - this.state.model.covariates.splice(idx, 1); - this.applyState(); - this.CovariatesModel.updateComponent(); - return this; - }.bind(this.parent_plot), - /** + removeByIdx: function (idx) { + if (typeof this.state.model.covariates[idx] == 'undefined') { + throw 'Unable to remove model covariate, invalid index: ' + idx.toString(); + } + this.state.model.covariates.splice(idx, 1); + this.applyState(); + this.CovariatesModel.updateComponent(); + return this; + }.bind(this.parent_plot), + /** * Empty the `state.model.covariates` array (and dashboard component menu representation thereof) of all * elements. When plot state is changed, this will automatically trigger requests for new data accordingly */ - removeAll: function(){ - this.state.model.covariates = []; - this.applyState(); - this.CovariatesModel.updateComponent(); - return this; - }.bind(this.parent_plot), - /** + removeAll: function () { + this.state.model.covariates = []; + this.applyState(); + this.CovariatesModel.updateComponent(); + return this; + }.bind(this.parent_plot), + /** * Manually trigger the update methods on the dashboard component's button and menu elements to force * display of most up-to-date content. Can be used to force the dashboard to reflect changes made, eg if * modifying `state.model.covariates` directly instead of via `plot.CovariatesModel` */ - updateComponent: function(){ - this.button.update(); - this.button.menu.update(); - }.bind(this) - }; - }.bind(this); - - this.update = function(){ - - if (this.button){ return this; } - - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title) - .setOnclick(function(){ - this.button.menu.populate(); - }.bind(this)); - - this.button.menu.setPopulate(function(){ - var selector = this.button.menu.inner_selector; - selector.html(""); - // General model HTML representation - if (typeof this.parent_plot.state.model.html != "undefined"){ - selector.append("div").html(this.parent_plot.state.model.html); - } - // Model covariates table - if (!this.parent_plot.state.model.covariates.length){ - selector.append("i").html("no covariates in model"); - } else { - selector.append("h5").html("Model Covariates (" + this.parent_plot.state.model.covariates.length + ")"); - var table = selector.append("table"); - this.parent_plot.state.model.covariates.forEach(function(covariate, idx){ - var html = ( (typeof covariate == "object" && typeof covariate.html == "string") ? covariate.html : covariate.toString() ); - var row = table.append("tr"); - row.append("td").append("button") - .attr("class", "lz-dashboard-button lz-dashboard-button-" + this.layout.color) - .style({ "margin-left": "0em" }) - .on("click", function(){ - this.parent_plot.CovariatesModel.removeByIdx(idx); - }.bind(this)) - .html("×"); - row.append("td").html(html); + updateComponent: function () { + this.button.update(); + this.button.menu.update(); + }.bind(this) + }; + }.bind(this); + this.update = function () { + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title).setOnclick(function () { + this.button.menu.populate(); }.bind(this)); - selector.append("button") - .attr("class", "lz-dashboard-button lz-dashboard-button-" + this.layout.color) - .style({ "margin-left": "4px" }).html("× Remove All Covariates") - .on("click", function(){ - this.parent_plot.CovariatesModel.removeAll(); - }.bind(this)); - } - }.bind(this)); - - this.button.preUpdate = function(){ - var html = "Model"; - if (this.parent_plot.state.model.covariates.length){ - var cov = this.parent_plot.state.model.covariates.length > 1 ? "covariates" : "covariate"; - html += " (" + this.parent_plot.state.model.covariates.length + " " + cov + ")"; - } - this.button.setHtml(html).disable(false); - }.bind(this); - - this.button.show(); - - return this; - }; -}); - -/** + this.button.menu.setPopulate(function () { + var selector = this.button.menu.inner_selector; + selector.html(''); + // General model HTML representation + if (typeof this.parent_plot.state.model.html != 'undefined') { + selector.append('div').html(this.parent_plot.state.model.html); + } + // Model covariates table + if (!this.parent_plot.state.model.covariates.length) { + selector.append('i').html('no covariates in model'); + } else { + selector.append('h5').html('Model Covariates (' + this.parent_plot.state.model.covariates.length + ')'); + var table = selector.append('table'); + this.parent_plot.state.model.covariates.forEach(function (covariate, idx) { + var html = typeof covariate == 'object' && typeof covariate.html == 'string' ? covariate.html : covariate.toString(); + var row = table.append('tr'); + row.append('td').append('button').attr('class', 'lz-dashboard-button lz-dashboard-button-' + this.layout.color).style({ 'margin-left': '0em' }).on('click', function () { + this.parent_plot.CovariatesModel.removeByIdx(idx); + }.bind(this)).html('\xD7'); + row.append('td').html(html); + }.bind(this)); + selector.append('button').attr('class', 'lz-dashboard-button lz-dashboard-button-' + this.layout.color).style({ 'margin-left': '4px' }).html('\xD7 Remove All Covariates').on('click', function () { + this.parent_plot.CovariatesModel.removeAll(); + }.bind(this)); + } + }.bind(this)); + this.button.preUpdate = function () { + var html = 'Model'; + if (this.parent_plot.state.model.covariates.length) { + var cov = this.parent_plot.state.model.covariates.length > 1 ? 'covariates' : 'covariate'; + html += ' (' + this.parent_plot.state.model.covariates.length + ' ' + cov + ')'; + } + this.button.setHtml(html).disable(false); + }.bind(this); + this.button.show(); + return this; + }; + }); + /** * Button to toggle split tracks * @class LocusZoom.Dashboard.Components.toggle_split_tracks * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("toggle_split_tracks", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - if (!layout.data_layer_id){ layout.data_layer_id = "intervals"; } - if (!this.parent_panel.data_layers[layout.data_layer_id]){ - throw ("Dashboard toggle split tracks component missing valid data layer ID"); - } - this.update = function(){ - var data_layer = this.parent_panel.data_layers[layout.data_layer_id]; - var html = data_layer.layout.split_tracks ? "Merge Tracks" : "Split Tracks"; - if (this.button){ - this.button.setHtml(html); - this.button.show(); - this.parent.position(); - return this; - } else { - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(html) - .setTitle("Toggle whether tracks are split apart or merged together") - .setOnclick(function(){ - data_layer.toggleSplitTracks(); - if (this.scale_timeout){ clearTimeout(this.scale_timeout); } - var timeout = data_layer.layout.transition ? +data_layer.layout.transition.duration || 0 : 0; - this.scale_timeout = setTimeout(function(){ - this.parent_panel.scaleHeightToData(); - this.parent_plot.positionPanels(); - }.bind(this), timeout); - this.update(); - }.bind(this)); - return this.update(); - } - }; -}); - -/** + LocusZoom.Dashboard.Components.add('toggle_split_tracks', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + if (!layout.data_layer_id) { + layout.data_layer_id = 'intervals'; + } + if (!this.parent_panel.data_layers[layout.data_layer_id]) { + throw 'Dashboard toggle split tracks component missing valid data layer ID'; + } + this.update = function () { + var data_layer = this.parent_panel.data_layers[layout.data_layer_id]; + var html = data_layer.layout.split_tracks ? 'Merge Tracks' : 'Split Tracks'; + if (this.button) { + this.button.setHtml(html); + this.button.show(); + this.parent.position(); + return this; + } else { + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(html).setTitle('Toggle whether tracks are split apart or merged together').setOnclick(function () { + data_layer.toggleSplitTracks(); + if (this.scale_timeout) { + clearTimeout(this.scale_timeout); + } + var timeout = data_layer.layout.transition ? +data_layer.layout.transition.duration || 0 : 0; + this.scale_timeout = setTimeout(function () { + this.parent_panel.scaleHeightToData(); + this.parent_plot.positionPanels(); + }.bind(this), timeout); + this.update(); + }.bind(this)); + return this.update(); + } + }; + }); + /** * Button to resize panel height to fit available data (eg when showing a list of tracks) * @class LocusZoom.Dashboard.Components.resize_to_data * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("resize_to_data", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - if (this.button){ return this; } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml("Resize to Data") - .setTitle("Automatically resize this panel to fit the data its currently showing") - .setOnclick(function(){ - this.parent_panel.scaleHeightToData(); - this.update(); - }.bind(this)); - this.button.show(); - return this; - }; -}); - -/** + LocusZoom.Dashboard.Components.add('resize_to_data', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml('Resize to Data').setTitle('Automatically resize this panel to fit the data its currently showing').setOnclick(function () { + this.parent_panel.scaleHeightToData(); + this.update(); + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Button to toggle legend * @class LocusZoom.Dashboard.Components.toggle_legend * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("toggle_legend", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - this.update = function(){ - var html = this.parent_panel.legend.layout.hidden ? "Show Legend" : "Hide Legend"; - if (this.button){ - this.button.setHtml(html).show(); - this.parent.position(); - return this; - } - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color) - .setTitle("Show or hide the legend for this panel") - .setOnclick(function(){ - this.parent_panel.legend.layout.hidden = !this.parent_panel.legend.layout.hidden; - this.parent_panel.legend.render(); - this.update(); - }.bind(this)); - return this.update(); - }; -}); - -/** + LocusZoom.Dashboard.Components.add('toggle_legend', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + var html = this.parent_panel.legend.layout.hidden ? 'Show Legend' : 'Hide Legend'; + if (this.button) { + this.button.setHtml(html).show(); + this.parent.position(); + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setTitle('Show or hide the legend for this panel').setOnclick(function () { + this.parent_panel.legend.layout.hidden = !this.parent_panel.legend.layout.hidden; + this.parent_panel.legend.render(); + this.update(); + }.bind(this)); + return this.update(); + }; + }); + /** * Menu for manipulating multiple data layers in a single panel: show/hide, change order, etc. * @class LocusZoom.Dashboard.Components.data_layers * @augments LocusZoom.Dashboard.Component */ -LocusZoom.Dashboard.Components.add("data_layers", function(layout){ - LocusZoom.Dashboard.Component.apply(this, arguments); - - this.update = function(){ - - if (typeof layout.button_html != "string"){ layout.button_html = "Data Layers"; } - if (typeof layout.button_title != "string"){ layout.button_title = "Manipulate Data Layers (sort, dim, show/hide, etc.)"; } - - if (this.button){ return this; } - - this.button = new LocusZoom.Dashboard.Component.Button(this) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title) - .setOnclick(function(){ - this.button.menu.populate(); - }.bind(this)); - - this.button.menu.setPopulate(function(){ - this.button.menu.inner_selector.html(""); - var table = this.button.menu.inner_selector.append("table"); - this.parent_panel.data_layer_ids_by_z_index.slice().reverse().forEach(function(id, idx){ - var data_layer = this.parent_panel.data_layers[id]; - var name = (typeof data_layer.layout.name != "string") ? data_layer.id : data_layer.layout.name; - var row = table.append("tr"); - // Layer name - row.append("td").html(name); - // Status toggle buttons - layout.statuses.forEach(function(status_adj){ - var status_idx = LocusZoom.DataLayer.Statuses.adjectives.indexOf(status_adj); - var status_verb = LocusZoom.DataLayer.Statuses.verbs[status_idx]; - var html, onclick, highlight; - if (data_layer.global_statuses[status_adj]){ - html = LocusZoom.DataLayer.Statuses.menu_antiverbs[status_idx]; - onclick = "un" + status_verb + "AllElements"; - highlight = "-highlighted"; - } else { - html = LocusZoom.DataLayer.Statuses.verbs[status_idx]; - onclick = status_verb + "AllElements"; - highlight = ""; - } - row.append("td").append("a") - .attr("class", "lz-dashboard-button lz-dashboard-button-" + this.layout.color + highlight) - .style({ "margin-left": "0em" }) - .on("click", function(){ data_layer[onclick](); this.button.menu.populate(); }.bind(this)) - .html(html); + LocusZoom.Dashboard.Components.add('data_layers', function (layout) { + LocusZoom.Dashboard.Component.apply(this, arguments); + this.update = function () { + if (typeof layout.button_html != 'string') { + layout.button_html = 'Data Layers'; + } + if (typeof layout.button_title != 'string') { + layout.button_title = 'Manipulate Data Layers (sort, dim, show/hide, etc.)'; + } + if (this.button) { + return this; + } + this.button = new LocusZoom.Dashboard.Component.Button(this).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title).setOnclick(function () { + this.button.menu.populate(); }.bind(this)); - // Sort layer buttons - var at_top = (idx === 0); - var at_bottom = (idx === (this.parent_panel.data_layer_ids_by_z_index.length - 1)); - var td = row.append("td"); - td.append("a") - .attr("class", "lz-dashboard-button lz-dashboard-button-group-start lz-dashboard-button-" + this.layout.color + (at_bottom ? "-disabled" : "")) - .style({ "margin-left": "0em" }) - .on("click", function(){ data_layer.moveDown(); this.button.menu.populate(); }.bind(this)) - .html("▾").attr("title", "Move layer down (further back)"); - td.append("a") - .attr("class", "lz-dashboard-button lz-dashboard-button-group-middle lz-dashboard-button-" + this.layout.color + (at_top ? "-disabled" : "")) - .style({ "margin-left": "0em" }) - .on("click", function(){ data_layer.moveUp(); this.button.menu.populate(); }.bind(this)) - .html("▴").attr("title", "Move layer up (further front)"); - td.append("a") - .attr("class", "lz-dashboard-button lz-dashboard-button-group-end lz-dashboard-button-red") - .style({ "margin-left": "0em" }) - .on("click", function(){ - if (confirm("Are you sure you want to remove the " + name + " layer? This cannot be undone!")){ - data_layer.parent.removeDataLayer(id); - } - return this.button.menu.populate(); - }.bind(this)) - .html("×").attr("title", "Remove layer"); - }.bind(this)); - return this; - }.bind(this)); - - this.button.show(); - - return this; - }; -}); - -/** + this.button.menu.setPopulate(function () { + this.button.menu.inner_selector.html(''); + var table = this.button.menu.inner_selector.append('table'); + this.parent_panel.data_layer_ids_by_z_index.slice().reverse().forEach(function (id, idx) { + var data_layer = this.parent_panel.data_layers[id]; + var name = typeof data_layer.layout.name != 'string' ? data_layer.id : data_layer.layout.name; + var row = table.append('tr'); + // Layer name + row.append('td').html(name); + // Status toggle buttons + layout.statuses.forEach(function (status_adj) { + var status_idx = LocusZoom.DataLayer.Statuses.adjectives.indexOf(status_adj); + var status_verb = LocusZoom.DataLayer.Statuses.verbs[status_idx]; + var html, onclick, highlight; + if (data_layer.global_statuses[status_adj]) { + html = LocusZoom.DataLayer.Statuses.menu_antiverbs[status_idx]; + onclick = 'un' + status_verb + 'AllElements'; + highlight = '-highlighted'; + } else { + html = LocusZoom.DataLayer.Statuses.verbs[status_idx]; + onclick = status_verb + 'AllElements'; + highlight = ''; + } + row.append('td').append('a').attr('class', 'lz-dashboard-button lz-dashboard-button-' + this.layout.color + highlight).style({ 'margin-left': '0em' }).on('click', function () { + data_layer[onclick](); + this.button.menu.populate(); + }.bind(this)).html(html); + }.bind(this)); + // Sort layer buttons + var at_top = idx === 0; + var at_bottom = idx === this.parent_panel.data_layer_ids_by_z_index.length - 1; + var td = row.append('td'); + td.append('a').attr('class', 'lz-dashboard-button lz-dashboard-button-group-start lz-dashboard-button-' + this.layout.color + (at_bottom ? '-disabled' : '')).style({ 'margin-left': '0em' }).on('click', function () { + data_layer.moveDown(); + this.button.menu.populate(); + }.bind(this)).html('\u25BE').attr('title', 'Move layer down (further back)'); + td.append('a').attr('class', 'lz-dashboard-button lz-dashboard-button-group-middle lz-dashboard-button-' + this.layout.color + (at_top ? '-disabled' : '')).style({ 'margin-left': '0em' }).on('click', function () { + data_layer.moveUp(); + this.button.menu.populate(); + }.bind(this)).html('\u25B4').attr('title', 'Move layer up (further front)'); + td.append('a').attr('class', 'lz-dashboard-button lz-dashboard-button-group-end lz-dashboard-button-red').style({ 'margin-left': '0em' }).on('click', function () { + if (confirm('Are you sure you want to remove the ' + name + ' layer? This cannot be undone!')) { + data_layer.parent.removeDataLayer(id); + } + return this.button.menu.populate(); + }.bind(this)).html('\xD7').attr('title', 'Remove layer'); + }.bind(this)); + return this; + }.bind(this)); + this.button.show(); + return this; + }; + }); + /** * Dropdown menu allowing the user to choose between different display options for a single specific data layer * within a panel. * @@ -7373,92 +7419,91 @@ LocusZoom.Dashboard.Components.add("data_layers", function(layout){ * @param {DisplayOptionsButtonConfigField[]} layout.options Specify a label and set of layout directives associated * with this `display` option. Display field should include all changes to datalayer presentation options. */ -LocusZoom.Dashboard.Components.add("display_options", function (layout) { - if (typeof layout.button_html != "string"){ layout.button_html = "Display options"; } - if (typeof layout.button_title != "string"){ layout.button_title = "Control how plot items are displayed"; } - - // Call parent constructor - LocusZoom.Dashboard.Component.apply(this, arguments); - - // List of layout fields that this button is allowed to control. This ensures that we don't override any other - // information (like plot height etc) while changing point rendering - var allowed_fields = layout.fields_whitelist || ["color", "fill_opacity", "label", "legend", - "point_shape", "point_size", "tooltip", "tooltip_positioning"]; - - var dataLayer = this.parent_panel.data_layers[layout.layer_name]; - var dataLayerLayout = dataLayer.layout; - - // Store default configuration for the layer as a clean deep copy, so we may revert later - var defaultConfig = {}; - allowed_fields.forEach(function(name) { - var configSlot = dataLayerLayout[name]; - if (configSlot) { - defaultConfig[name] = JSON.parse(JSON.stringify(configSlot)); - } - }); - - /** + LocusZoom.Dashboard.Components.add('display_options', function (layout) { + if (typeof layout.button_html != 'string') { + layout.button_html = 'Display options'; + } + if (typeof layout.button_title != 'string') { + layout.button_title = 'Control how plot items are displayed'; + } + // Call parent constructor + LocusZoom.Dashboard.Component.apply(this, arguments); + // List of layout fields that this button is allowed to control. This ensures that we don't override any other + // information (like plot height etc) while changing point rendering + var allowed_fields = layout.fields_whitelist || [ + 'color', + 'fill_opacity', + 'label', + 'legend', + 'point_shape', + 'point_size', + 'tooltip', + 'tooltip_positioning' + ]; + var dataLayer = this.parent_panel.data_layers[layout.layer_name]; + var dataLayerLayout = dataLayer.layout; + // Store default configuration for the layer as a clean deep copy, so we may revert later + var defaultConfig = {}; + allowed_fields.forEach(function (name) { + var configSlot = dataLayerLayout[name]; + if (configSlot) { + defaultConfig[name] = JSON.parse(JSON.stringify(configSlot)); + } + }); + /** * Which item in the menu is currently selected. (track for rerendering menu) * @member {String} * @private */ - this._selected_item = "default"; - - // Define the button + menu that provides the real functionality for this dashboard component - var self = this; - this.button = new LocusZoom.Dashboard.Component.Button(self) - .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title) - .setOnclick(function () { - self.button.menu.populate(); - }); - this.button.menu.setPopulate(function () { - // Multiple copies of this button might be used on a single LZ page; append unique IDs where needed - var uniqueID = Math.floor(Math.random() * 1e4).toString(); - - self.button.menu.inner_selector.html(""); - var table = self.button.menu.inner_selector.append("table"); - - var menuLayout = self.layout; - - var renderRow = function(display_name, display_options, row_id) { // Helper method - var row = table.append("tr"); - row.append("td") - .append("input") - .attr({type: "radio", name: "color-picker-" + uniqueID, value: row_id}) - .property("checked", (row_id === self._selected_item)) - .on("click", function () { - Object.keys(display_options).forEach(function(field_name) { - dataLayer.layout[field_name] = display_options[field_name]; + this._selected_item = 'default'; + // Define the button + menu that provides the real functionality for this dashboard component + var self = this; + this.button = new LocusZoom.Dashboard.Component.Button(self).setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title).setOnclick(function () { + self.button.menu.populate(); + }); + this.button.menu.setPopulate(function () { + // Multiple copies of this button might be used on a single LZ page; append unique IDs where needed + var uniqueID = Math.floor(Math.random() * 10000).toString(); + self.button.menu.inner_selector.html(''); + var table = self.button.menu.inner_selector.append('table'); + var menuLayout = self.layout; + var renderRow = function (display_name, display_options, row_id) { + // Helper method + var row = table.append('tr'); + row.append('td').append('input').attr({ + type: 'radio', + name: 'color-picker-' + uniqueID, + value: row_id + }).property('checked', row_id === self._selected_item).on('click', function () { + Object.keys(display_options).forEach(function (field_name) { + dataLayer.layout[field_name] = display_options[field_name]; + }); + self._selected_item = row_id; + self.parent_panel.render(); + var legend = self.parent_panel.legend; + if (legend && display_options.legend) { + // Update the legend only if necessary + legend.render(); + } }); - self._selected_item = row_id; - self.parent_panel.render(); - var legend = self.parent_panel.legend; - if (legend && display_options.legend) { - // Update the legend only if necessary - legend.render(); - } + row.append('td').text(display_name); + }; + // Render the "display options" menu: default and special custom options + var defaultName = menuLayout.default_config_display_name || 'Default style'; + renderRow(defaultName, defaultConfig, 'default'); + menuLayout.options.forEach(function (item, index) { + renderRow(item.display_name, item.display, index); }); - row.append("td").text(display_name); - }; - // Render the "display options" menu: default and special custom options - var defaultName = menuLayout.default_config_display_name || "Default style"; - renderRow(defaultName, defaultConfig, "default"); - menuLayout.options.forEach(function (item, index) { - renderRow(item.display_name, item.display, index); - }); - return self; - }); - - this.update = function () { - this.button.show(); - return this; - }; -}); - -/* global LocusZoom */ -"use strict"; - -/** + return self; + }); + this.update = function () { + this.button.show(); + return this; + }; + }); + /* global LocusZoom */ + 'use strict'; + /** * An SVG object used to display contextual information about a panel. * Panel layouts determine basic features of a legend - its position in the panel, orientation, title, etc. * Layouts of child data layers of the panel determine the actual content of the legend. @@ -7466,328 +7511,287 @@ LocusZoom.Dashboard.Components.add("display_options", function (layout) { * @class * @param {LocusZoom.Panel} parent */ -LocusZoom.Legend = function(parent){ - if (!(parent instanceof LocusZoom.Panel)){ - throw "Unable to create legend, parent must be a locuszoom panel"; - } - /** @member {LocusZoom.Panel} */ - this.parent = parent; - /** @member {String} */ - this.id = this.parent.getBaseId() + ".legend"; - - this.parent.layout.legend = LocusZoom.Layouts.merge(this.parent.layout.legend || {}, LocusZoom.Legend.DefaultLayout); - /** @member {Object} */ - this.layout = this.parent.layout.legend; - - /** @member {d3.selection} */ - this.selector = null; - /** @member {d3.selection} */ - this.background_rect = null; - /** @member {d3.selection[]} */ - this.elements = []; - /** + LocusZoom.Legend = function (parent) { + if (!(parent instanceof LocusZoom.Panel)) { + throw 'Unable to create legend, parent must be a locuszoom panel'; + } + /** @member {LocusZoom.Panel} */ + this.parent = parent; + /** @member {String} */ + this.id = this.parent.getBaseId() + '.legend'; + this.parent.layout.legend = LocusZoom.Layouts.merge(this.parent.layout.legend || {}, LocusZoom.Legend.DefaultLayout); + /** @member {Object} */ + this.layout = this.parent.layout.legend; + /** @member {d3.selection} */ + this.selector = null; + /** @member {d3.selection} */ + this.background_rect = null; + /** @member {d3.selection[]} */ + this.elements = []; + /** * SVG selector for the group containing all elements in the legend * @protected * @member {d3.selection|null} */ - this.elements_group = null; - - /** + this.elements_group = null; + /** * TODO: Not sure if this property is used; the external-facing methods are setting `layout.hidden` instead. Tentatively mark deprecated. * @deprecated * @protected * @member {Boolean} */ - this.hidden = false; - - // TODO Revisit constructor return value; see https://stackoverflow.com/a/3350364/1422268 - return this.render(); -}; - -/** + this.hidden = false; + // TODO Revisit constructor return value; see https://stackoverflow.com/a/3350364/1422268 + return this.render(); + }; + /** * The default layout used by legends (used internally) * @protected * @member {Object} */ -LocusZoom.Legend.DefaultLayout = { - orientation: "vertical", - origin: { x: 0, y: 0 }, - width: 10, - height: 10, - padding: 5, - label_size: 12, - hidden: false -}; - -/** + LocusZoom.Legend.DefaultLayout = { + orientation: 'vertical', + origin: { + x: 0, + y: 0 + }, + width: 10, + height: 10, + padding: 5, + label_size: 12, + hidden: false + }; + /** * Render the legend in the parent panel */ -LocusZoom.Legend.prototype.render = function(){ - - // Get a legend group selector if not yet defined - if (!this.selector){ - this.selector = this.parent.svg.group.append("g") - .attr("id", this.parent.getBaseId() + ".legend").attr("class", "lz-legend"); - } - - // Get a legend background rect selector if not yet defined - if (!this.background_rect){ - this.background_rect = this.selector.append("rect") - .attr("width", 100).attr("height", 100).attr("class", "lz-legend-background"); - } - - // Get a legend elements group selector if not yet defined - if (!this.elements_group){ - this.elements_group = this.selector.append("g"); - } - - // Remove all elements from the document and re-render from scratch - this.elements.forEach(function(element){ - element.remove(); - }); - this.elements = []; - - // Gather all elements from data layers in order (top to bottom) and render them - var padding = +this.layout.padding || 1; - var x = padding; - var y = padding; - var line_height = 0; - this.parent.data_layer_ids_by_z_index.slice().reverse().forEach(function(id){ - if (Array.isArray(this.parent.data_layers[id].layout.legend)){ - this.parent.data_layers[id].layout.legend.forEach(function(element){ - var selector = this.elements_group.append("g") - .attr("transform", "translate(" + x + "," + y + ")"); - var label_size = +element.label_size || +this.layout.label_size || 12; - var label_x = 0; - var label_y = (label_size/2) + (padding/2); - line_height = Math.max(line_height, label_size + padding); - // Draw the legend element symbol (line, rect, shape, etc) - if (element.shape === "line"){ - // Line symbol - var length = +element.length || 16; - var path_y = (label_size/4) + (padding/2); - selector.append("path").attr("class", element.class || "") - .attr("d", "M0," + path_y + "L" + length + "," + path_y) - .style(element.style || {}); - label_x = length + padding; - } else if (element.shape === "rect"){ - // Rect symbol - var width = +element.width || 16; - var height = +element.height || width; - selector.append("rect").attr("class", element.class || "") - .attr("width", width).attr("height", height) - .attr("fill", element.color || {}) - .style(element.style || {}); - label_x = width + padding; - line_height = Math.max(line_height, height + padding); - } else if (d3.svg.symbolTypes.indexOf(element.shape) !== -1) { - // Shape symbol (circle, diamond, etc.) - var size = +element.size || 40; - var radius = Math.ceil(Math.sqrt(size/Math.PI)); - selector.append("path").attr("class", element.class || "") - .attr("d", d3.svg.symbol().size(size).type(element.shape)) - .attr("transform", "translate(" + radius + "," + (radius+(padding/2)) + ")") - .attr("fill", element.color || {}) - .style(element.style || {}); - label_x = (2*radius) + padding; - label_y = Math.max((2*radius)+(padding/2), label_y); - line_height = Math.max(line_height, (2*radius) + padding); - } - // Draw the legend element label - selector.append("text").attr("text-anchor", "left").attr("class", "lz-label") - .attr("x", label_x).attr("y", label_y).style({"font-size": label_size}).text(element.label); - // Position the legend element group based on legend layout orientation - var bcr = selector.node().getBoundingClientRect(); - if (this.layout.orientation === "vertical"){ - y += bcr.height + padding; - line_height = 0; - } else { - // Ensure this element does not exceed the panel width - // (E.g. drop to the next line if it does, but only if it's not the only element on this line) - var right_x = this.layout.origin.x + x + bcr.width; - if (x > padding && right_x > this.parent.layout.width){ - y += line_height; - x = padding; - selector.attr("transform", "translate(" + x + "," + y + ")"); - } - x += bcr.width + (3*padding); - } - // Store the element - this.elements.push(selector); + LocusZoom.Legend.prototype.render = function () { + // Get a legend group selector if not yet defined + if (!this.selector) { + this.selector = this.parent.svg.group.append('g').attr('id', this.parent.getBaseId() + '.legend').attr('class', 'lz-legend'); + } + // Get a legend background rect selector if not yet defined + if (!this.background_rect) { + this.background_rect = this.selector.append('rect').attr('width', 100).attr('height', 100).attr('class', 'lz-legend-background'); + } + // Get a legend elements group selector if not yet defined + if (!this.elements_group) { + this.elements_group = this.selector.append('g'); + } + // Remove all elements from the document and re-render from scratch + this.elements.forEach(function (element) { + element.remove(); + }); + this.elements = []; + // Gather all elements from data layers in order (top to bottom) and render them + var padding = +this.layout.padding || 1; + var x = padding; + var y = padding; + var line_height = 0; + this.parent.data_layer_ids_by_z_index.slice().reverse().forEach(function (id) { + if (Array.isArray(this.parent.data_layers[id].layout.legend)) { + this.parent.data_layers[id].layout.legend.forEach(function (element) { + var selector = this.elements_group.append('g').attr('transform', 'translate(' + x + ',' + y + ')'); + var label_size = +element.label_size || +this.layout.label_size || 12; + var label_x = 0; + var label_y = label_size / 2 + padding / 2; + line_height = Math.max(line_height, label_size + padding); + // Draw the legend element symbol (line, rect, shape, etc) + if (element.shape === 'line') { + // Line symbol + var length = +element.length || 16; + var path_y = label_size / 4 + padding / 2; + selector.append('path').attr('class', element.class || '').attr('d', 'M0,' + path_y + 'L' + length + ',' + path_y).style(element.style || {}); + label_x = length + padding; + } else if (element.shape === 'rect') { + // Rect symbol + var width = +element.width || 16; + var height = +element.height || width; + selector.append('rect').attr('class', element.class || '').attr('width', width).attr('height', height).attr('fill', element.color || {}).style(element.style || {}); + label_x = width + padding; + line_height = Math.max(line_height, height + padding); + } else if (d3.svg.symbolTypes.indexOf(element.shape) !== -1) { + // Shape symbol (circle, diamond, etc.) + var size = +element.size || 40; + var radius = Math.ceil(Math.sqrt(size / Math.PI)); + selector.append('path').attr('class', element.class || '').attr('d', d3.svg.symbol().size(size).type(element.shape)).attr('transform', 'translate(' + radius + ',' + (radius + padding / 2) + ')').attr('fill', element.color || {}).style(element.style || {}); + label_x = 2 * radius + padding; + label_y = Math.max(2 * radius + padding / 2, label_y); + line_height = Math.max(line_height, 2 * radius + padding); + } + // Draw the legend element label + selector.append('text').attr('text-anchor', 'left').attr('class', 'lz-label').attr('x', label_x).attr('y', label_y).style({ 'font-size': label_size }).text(element.label); + // Position the legend element group based on legend layout orientation + var bcr = selector.node().getBoundingClientRect(); + if (this.layout.orientation === 'vertical') { + y += bcr.height + padding; + line_height = 0; + } else { + // Ensure this element does not exceed the panel width + // (E.g. drop to the next line if it does, but only if it's not the only element on this line) + var right_x = this.layout.origin.x + x + bcr.width; + if (x > padding && right_x > this.parent.layout.width) { + y += line_height; + x = padding; + selector.attr('transform', 'translate(' + x + ',' + y + ')'); + } + x += bcr.width + 3 * padding; + } + // Store the element + this.elements.push(selector); + }.bind(this)); + } }.bind(this)); - } - }.bind(this)); - - // Scale the background rect to the elements in the legend - var bcr = this.elements_group.node().getBoundingClientRect(); - this.layout.width = bcr.width + (2*this.layout.padding); - this.layout.height = bcr.height + (2*this.layout.padding); - this.background_rect - .attr("width", this.layout.width) - .attr("height", this.layout.height); - - // Set the visibility on the legend from the "hidden" flag - // TODO: `show()` and `hide()` call a full rerender; might be able to make this more lightweight? - this.selector.style({ visibility: this.layout.hidden ? "hidden" : "visible" }); - - // TODO: Annotate return type and make consistent - return this.position(); -}; - -/** + // Scale the background rect to the elements in the legend + var bcr = this.elements_group.node().getBoundingClientRect(); + this.layout.width = bcr.width + 2 * this.layout.padding; + this.layout.height = bcr.height + 2 * this.layout.padding; + this.background_rect.attr('width', this.layout.width).attr('height', this.layout.height); + // Set the visibility on the legend from the "hidden" flag + // TODO: `show()` and `hide()` call a full rerender; might be able to make this more lightweight? + this.selector.style({ visibility: this.layout.hidden ? 'hidden' : 'visible' }); + // TODO: Annotate return type and make consistent + return this.position(); + }; + /** * Place the legend in position relative to the panel, as specified in the layout configuration * @returns {LocusZoom.Legend | null} * TODO: should this always be chainable? */ -LocusZoom.Legend.prototype.position = function(){ - if (!this.selector){ return this; } - var bcr = this.selector.node().getBoundingClientRect(); - if (!isNaN(+this.layout.pad_from_bottom)){ - this.layout.origin.y = this.parent.layout.height - bcr.height - +this.layout.pad_from_bottom; - } - if (!isNaN(+this.layout.pad_from_right)){ - this.layout.origin.x = this.parent.layout.width - bcr.width - +this.layout.pad_from_right; - } - this.selector.attr("transform", "translate(" + this.layout.origin.x + "," + this.layout.origin.y + ")"); -}; - -/** + LocusZoom.Legend.prototype.position = function () { + if (!this.selector) { + return this; + } + var bcr = this.selector.node().getBoundingClientRect(); + if (!isNaN(+this.layout.pad_from_bottom)) { + this.layout.origin.y = this.parent.layout.height - bcr.height - +this.layout.pad_from_bottom; + } + if (!isNaN(+this.layout.pad_from_right)) { + this.layout.origin.x = this.parent.layout.width - bcr.width - +this.layout.pad_from_right; + } + this.selector.attr('transform', 'translate(' + this.layout.origin.x + ',' + this.layout.origin.y + ')'); + }; + /** * Hide the legend (triggers a re-render) * @public */ -LocusZoom.Legend.prototype.hide = function(){ - this.layout.hidden = true; - this.render(); -}; - -/** + LocusZoom.Legend.prototype.hide = function () { + this.layout.hidden = true; + this.render(); + }; + /** * Show the legend (triggers a re-render) * @public */ -LocusZoom.Legend.prototype.show = function(){ - this.layout.hidden = false; - this.render(); -}; - -/* global LocusZoom */ -"use strict"; - -/** + LocusZoom.Legend.prototype.show = function () { + this.layout.hidden = false; + this.render(); + }; + /* global LocusZoom */ + 'use strict'; + /** * LocusZoom functionality used for data parsing and retrieval * @namespace * @public */ -LocusZoom.Data = LocusZoom.Data || {}; - -/** + LocusZoom.Data = LocusZoom.Data || {}; + /** * Create and coordinate an ensemble of (namespaced) data source instances * @public * @class */ -LocusZoom.DataSources = function() { - /** @member {Object.} */ - this.sources = {}; -}; - -/** @deprecated */ -LocusZoom.DataSources.prototype.addSource = function(ns, x) { - console.warn("Warning: .addSource() is deprecated. Use .add() instead"); - return this.add(ns, x); -}; - -/** + LocusZoom.DataSources = function () { + /** @member {Object.} */ + this.sources = {}; + }; + /** @deprecated */ + LocusZoom.DataSources.prototype.addSource = function (ns, x) { + console.warn('Warning: .addSource() is deprecated. Use .add() instead'); + return this.add(ns, x); + }; + /** * Add a (namespaced) datasource to the plot * @public * @param {String} ns A namespace used for fields from this data source * @param {LocusZoom.Data.Source|Array|null} x An instantiated datasource, or an array of arguments that can be used to * create a known datasource type. */ -LocusZoom.DataSources.prototype.add = function(ns, x) { - return this.set(ns, x); -}; - -/** @protected */ -LocusZoom.DataSources.prototype.set = function(ns, x) { - if (Array.isArray(x)) { - var dsobj = LocusZoom.KnownDataSources.create.apply(null, x); - this.sources[ns] = dsobj; - } else { - if (x !== null) { - this.sources[ns] = x; - } else { - delete this.sources[ns]; - } - } - return this; -}; - -/** @deprecated */ -LocusZoom.DataSources.prototype.getSource = function(ns) { - console.warn("Warning: .getSource() is deprecated. Use .get() instead"); - return this.get(ns); -}; - -/** + LocusZoom.DataSources.prototype.add = function (ns, x) { + return this.set(ns, x); + }; + /** @protected */ + LocusZoom.DataSources.prototype.set = function (ns, x) { + if (Array.isArray(x)) { + var dsobj = LocusZoom.KnownDataSources.create.apply(null, x); + this.sources[ns] = dsobj; + } else { + if (x !== null) { + this.sources[ns] = x; + } else { + delete this.sources[ns]; + } + } + return this; + }; + /** @deprecated */ + LocusZoom.DataSources.prototype.getSource = function (ns) { + console.warn('Warning: .getSource() is deprecated. Use .get() instead'); + return this.get(ns); + }; + /** * Return the datasource associated with a given namespace * @public * @param {String} ns Namespace * @returns {LocusZoom.Data.Source} */ -LocusZoom.DataSources.prototype.get = function(ns) { - return this.sources[ns]; -}; - -/** @deprecated */ -LocusZoom.DataSources.prototype.removeSource = function(ns) { - console.warn("Warning: .removeSource() is deprecated. Use .remove() instead"); - return this.remove(ns); -}; - -/** + LocusZoom.DataSources.prototype.get = function (ns) { + return this.sources[ns]; + }; + /** @deprecated */ + LocusZoom.DataSources.prototype.removeSource = function (ns) { + console.warn('Warning: .removeSource() is deprecated. Use .remove() instead'); + return this.remove(ns); + }; + /** * Remove the datasource associated with a given namespace * @public * @param {String} ns Namespace */ -LocusZoom.DataSources.prototype.remove = function(ns) { - return this.set(ns, null); -}; - -/** + LocusZoom.DataSources.prototype.remove = function (ns) { + return this.set(ns, null); + }; + /** * Populate a list of datasources specified as a JSON object * @public * @param {String|Object} x An object or JSON representation containing {ns: configArray} entries * @returns {LocusZoom.DataSources} */ -LocusZoom.DataSources.prototype.fromJSON = function(x) { - if (typeof x === "string") { - x = JSON.parse(x); - } - var ds = this; - Object.keys(x).forEach(function(ns) { - ds.set(ns, x[ns]); - }); - return ds; -}; - -/** + LocusZoom.DataSources.prototype.fromJSON = function (x) { + if (typeof x === 'string') { + x = JSON.parse(x); + } + var ds = this; + Object.keys(x).forEach(function (ns) { + ds.set(ns, x[ns]); + }); + return ds; + }; + /** * Return the names of all currently recognized datasources * @public * @returns {Array} */ -LocusZoom.DataSources.prototype.keys = function() { - return Object.keys(this.sources); -}; - -/** + LocusZoom.DataSources.prototype.keys = function () { + return Object.keys(this.sources); + }; + /** * Datasources can be instantiated from a JSON object instead of code. This represents existing sources in that format. * For example, this can be helpful when sharing plots, or to share settings with others when debugging * @public */ -LocusZoom.DataSources.prototype.toJSON = function() { - return this.sources; -}; - -/** + LocusZoom.DataSources.prototype.toJSON = function () { + return this.sources; + }; + /** * Represents an addressable unit of data from a namespaced datasource, subject to specified value transformations. * * When used by a data layer, fields will automatically be re-fetched from the appropriate data source whenever the @@ -7800,48 +7804,45 @@ LocusZoom.DataSources.prototype.toJSON = function() { * transformation(s) are optional and information is delimited according to the general syntax * `[namespace:]name[|transformation][|transformation]`. For example, `association:pvalue|neglog10` */ -LocusZoom.Data.Field = function(field){ - - var parts = /^(?:([^:]+):)?([^:|]*)(\|.+)*$/.exec(field); - /** @member {String} */ - this.full_name = field; - /** @member {String} */ - this.namespace = parts[1] || null; - /** @member {String} */ - this.name = parts[2] || null; - /** @member {Array} */ - this.transformations = []; - - if (typeof parts[3] == "string" && parts[3].length > 1){ - this.transformations = parts[3].substring(1).split("|"); - this.transformations.forEach(function(transform, i){ - this.transformations[i] = LocusZoom.TransformationFunctions.get(transform); - }.bind(this)); - } - - this.applyTransformations = function(val){ - this.transformations.forEach(function(transform){ - val = transform(val); - }); - return val; - }; - - // Resolve the field for a given data element. - // First look for a full match with transformations already applied by the data requester. - // Otherwise prefer a namespace match and fall back to just a name match, applying transformations on the fly. - this.resolve = function(d){ - if (typeof d[this.full_name] == "undefined"){ - var val = null; - if (typeof (d[this.namespace+":"+this.name]) != "undefined"){ val = d[this.namespace+":"+this.name]; } - else if (typeof d[this.name] != "undefined"){ val = d[this.name]; } - d[this.full_name] = this.applyTransformations(val); - } - return d[this.full_name]; - }; - -}; - -/** + LocusZoom.Data.Field = function (field) { + var parts = /^(?:([^:]+):)?([^:|]*)(\|.+)*$/.exec(field); + /** @member {String} */ + this.full_name = field; + /** @member {String} */ + this.namespace = parts[1] || null; + /** @member {String} */ + this.name = parts[2] || null; + /** @member {Array} */ + this.transformations = []; + if (typeof parts[3] == 'string' && parts[3].length > 1) { + this.transformations = parts[3].substring(1).split('|'); + this.transformations.forEach(function (transform, i) { + this.transformations[i] = LocusZoom.TransformationFunctions.get(transform); + }.bind(this)); + } + this.applyTransformations = function (val) { + this.transformations.forEach(function (transform) { + val = transform(val); + }); + return val; + }; + // Resolve the field for a given data element. + // First look for a full match with transformations already applied by the data requester. + // Otherwise prefer a namespace match and fall back to just a name match, applying transformations on the fly. + this.resolve = function (d) { + if (typeof d[this.full_name] == 'undefined') { + var val = null; + if (typeof d[this.namespace + ':' + this.name] != 'undefined') { + val = d[this.namespace + ':' + this.name]; + } else if (typeof d[this.name] != 'undefined') { + val = d[this.name]; + } + d[this.full_name] = this.applyTransformations(val); + } + return d[this.full_name]; + }; + }; + /** * The Requester manages fetching of data across multiple data sources. It is used internally by LocusZoom data layers. * It passes state information and ensures that data is formatted in the manner expected by the plot. * @@ -7851,90 +7852,99 @@ LocusZoom.Data.Field = function(field){ * @param {LocusZoom.DataSources} sources An object of {ns: LocusZoom.Data.Source} instances * @class */ -LocusZoom.Data.Requester = function(sources) { - - function split_requests(fields) { - // Given a fields array, return an object specifying what datasource names the data layer should make requests - // to, and how to handle the returned data - var requests = {}; - // Regular expression finds namespace:field|trans - var re = /^(?:([^:]+):)?([^:|]*)(\|.+)*$/; - fields.forEach(function(raw) { - var parts = re.exec(raw); - var ns = parts[1] || "base"; - var field = parts[2]; - var trans = LocusZoom.TransformationFunctions.get(parts[3]); - if (typeof requests[ns] =="undefined") { - requests[ns] = {outnames:[], fields:[], trans:[]}; - } - requests[ns].outnames.push(raw); - requests[ns].fields.push(field); - requests[ns].trans.push(trans); - }); - return requests; - } - - /** + LocusZoom.Data.Requester = function (sources) { + function split_requests(fields) { + // Given a fields array, return an object specifying what datasource names the data layer should make requests + // to, and how to handle the returned data + var requests = {}; + // Regular expression finds namespace:field|trans + var re = /^(?:([^:]+):)?([^:|]*)(\|.+)*$/; + fields.forEach(function (raw) { + var parts = re.exec(raw); + var ns = parts[1] || 'base'; + var field = parts[2]; + var trans = LocusZoom.TransformationFunctions.get(parts[3]); + if (typeof requests[ns] == 'undefined') { + requests[ns] = { + outnames: [], + fields: [], + trans: [] + }; + } + requests[ns].outnames.push(raw); + requests[ns].fields.push(field); + requests[ns].trans.push(trans); + }); + return requests; + } + /** * Fetch data, and create a chain that only connects two data sources if they depend on each other * @param {Object} state The current "state" of the plot, such as chromosome and start/end positions * @param {String[]} fields The list of data fields specified in the `layout` for a specific data layer * @returns {Promise} */ - this.getData = function(state, fields) { - var requests = split_requests(fields); - // Create an array of functions that, when called, will trigger the request to the specified datasource - var promises = Object.keys(requests).map(function(key) { - if (!sources.get(key)) { - throw("Datasource for namespace " + key + " not found"); - } - return sources.get(key).getData(state, requests[key].fields, - requests[key].outnames, requests[key].trans); - }); - //assume the fields are requested in dependent order - //TODO: better manage dependencies - var ret = Q.when({header:{}, body:{}}); - for(var i=0; i < promises.length; i++) { - // If a single datalayer uses multiple sources, perform the next request when the previous one completes - ret = ret.then(promises[i]); - } - return ret; - }; -}; - -/** + this.getData = function (state, fields) { + var requests = split_requests(fields); + // Create an array of functions that, when called, will trigger the request to the specified datasource + var promises = Object.keys(requests).map(function (key) { + if (!sources.get(key)) { + throw 'Datasource for namespace ' + key + ' not found'; + } + return sources.get(key).getData(state, requests[key].fields, requests[key].outnames, requests[key].trans); + }); + //assume the fields are requested in dependent order + //TODO: better manage dependencies + var ret = Q.when({ + header: {}, + body: {} + }); + for (var i = 0; i < promises.length; i++) { + // If a single datalayer uses multiple sources, perform the next request when the previous one completes + ret = ret.then(promises[i]); + } + return ret; + }; + }; + /** * Base class for LocusZoom data sources * This can be extended with .extend() to create custom data sources * @class * @public */ -LocusZoom.Data.Source = function() { - /** @member {Boolean} */ - this.enableCache = true; -}; - -/** + LocusZoom.Data.Source = function () { + /** + * Whether this source should enable caching + * @member {Boolean} + */ + this.enableCache = true; + /** + * Whether this data source type is dependent on previous requests- for example, the LD source cannot annotate + * association data if no data was found for that region. + * @member {boolean} + */ + this.dependentSource = false; + }; + /** * A default constructor that can be used when creating new data sources * @param {String|Object} init Basic configuration- either a url, or a config object * @param {String} [init.url] The datasource URL * @param {String} [init.params] Initial config params for the datasource */ -LocusZoom.Data.Source.prototype.parseInit = function(init) { - if (typeof init === "string") { - /** @member {String} */ - this.url = init; - /** @member {String} */ - this.params = {}; - } else { - this.url = init.url; - this.params = init.params || {}; - } - if (!this.url) { - throw("Source not initialized with required URL"); - } - -}; - -/** + LocusZoom.Data.Source.prototype.parseInit = function (init) { + if (typeof init === 'string') { + /** @member {String} */ + this.url = init; + /** @member {String} */ + this.params = {}; + } else { + this.url = init.url; + this.params = init.params || {}; + } + if (!this.url) { + throw 'Source not initialized with required URL'; + } + }; + /** * Fetch the internal string used to represent this data when cache is used * @protected * @param state @@ -7942,47 +7952,43 @@ LocusZoom.Data.Source.prototype.parseInit = function(init) { * @param fields * @returns {String|undefined} */ -LocusZoom.Data.Source.prototype.getCacheKey = function(state, chain, fields) { - var url = this.getURL && this.getURL(state, chain, fields); - return url; -}; - -/** + LocusZoom.Data.Source.prototype.getCacheKey = function (state, chain, fields) { + var url = this.getURL && this.getURL(state, chain, fields); + return url; + }; + /** * Fetch data from a remote location * @protected * @param {Object} state The state of the parent plot * @param chain * @param fields */ -LocusZoom.Data.Source.prototype.fetchRequest = function(state, chain, fields) { - var url = this.getURL(state, chain, fields); - return LocusZoom.createCORSPromise("GET", url); -}; -// TODO: move this.getURL stub into parent class and add documentation; parent should not check for methods known only to children - - -/** + LocusZoom.Data.Source.prototype.fetchRequest = function (state, chain, fields) { + var url = this.getURL(state, chain, fields); + return LocusZoom.createCORSPromise('GET', url); + }; + // TODO: move this.getURL stub into parent class and add documentation; parent should not check for methods known only to children + /** * TODO Rename to handleRequest (to disambiguate from, say HTTP get requests) and update wiki docs and other references * @protected */ -LocusZoom.Data.Source.prototype.getRequest = function(state, chain, fields) { - var req; - var cacheKey = this.getCacheKey(state, chain, fields); - if (this.enableCache && typeof(cacheKey) !== "undefined" && cacheKey === this._cachedKey) { - req = Q.when(this._cachedResponse); - } else { - req = this.fetchRequest(state, chain, fields); - if (this.enableCache) { - req = req.then(function(x) { - this._cachedKey = cacheKey; - return this._cachedResponse = x; - }.bind(this)); - } - } - return req; -}; - -/** + LocusZoom.Data.Source.prototype.getRequest = function (state, chain, fields) { + var req; + var cacheKey = this.getCacheKey(state, chain, fields); + if (this.enableCache && typeof cacheKey !== 'undefined' && cacheKey === this._cachedKey) { + req = Q.when(this._cachedResponse); + } else { + req = this.fetchRequest(state, chain, fields); + if (this.enableCache) { + req = req.then(function (x) { + this._cachedKey = cacheKey; + return this._cachedResponse = x; + }.bind(this)); + } + } + return req; + }; + /** * Fetch the data from the specified data source, and format it in a way that can be used by the consuming plot * @protected * @param {Object} state The current "state" of the plot, such as chromosome and start/end positions @@ -7993,25 +7999,29 @@ LocusZoom.Data.Source.prototype.getRequest = function(state, chain, fields) { * This must be an array with the same length as `fields` * @returns {function(this:LocusZoom.Data.Source)} A callable operation that can be used as part of the data chain */ -LocusZoom.Data.Source.prototype.getData = function(state, fields, outnames, trans) { - if (this.preGetData) { - var pre = this.preGetData(state, fields, outnames, trans); - if(this.pre) { - state = pre.state || state; - fields = pre.fields || fields; - outnames = pre.outnames || outnames; - trans = pre.trans || trans; - } - } - - return function (chain) { - return this.getRequest(state, chain, fields).then(function(resp) { - return this.parseResponse(resp, chain, fields, outnames, trans); - }.bind(this)); - }.bind(this); -}; - -/** + LocusZoom.Data.Source.prototype.getData = function (state, fields, outnames, trans) { + if (this.preGetData) { + var pre = this.preGetData(state, fields, outnames, trans); + if (this.pre) { + state = pre.state || state; + fields = pre.fields || fields; + outnames = pre.outnames || outnames; + trans = pre.trans || trans; + } + } + var self = this; + return function (chain) { + if (self.dependentSource && chain && chain.body && !chain.body.length) { + // A "dependent" source should not attempt to fire a request if there is no data for it to act on. + // Therefore, it should simply return the previous data chain. + return Q.when(chain); + } + return self.getRequest(state, chain, fields).then(function (resp) { + return self.parseResponse(resp, chain, fields, outnames, trans); + }); + }; + }; + /** * Parse response data. Return an object containing "header" (metadata or request parameters) and "body" * (data to be used for plotting). The response from this request is combined with responses from all other requests * in the chain. @@ -8025,12 +8035,15 @@ LocusZoom.Data.Source.prototype.getData = function(state, fields, outnames, tran * This must be an array with the same length as `fields` * @returns {{header: ({}|*), body: {}}} */ -LocusZoom.Data.Source.prototype.parseResponse = function(resp, chain, fields, outnames, trans) { - var json = typeof resp == "string" ? JSON.parse(resp) : resp; - var records = this.parseData(json.data || json, fields, outnames, trans); - return {header: chain.header || {}, body: records}; -}; -/** + LocusZoom.Data.Source.prototype.parseResponse = function (resp, chain, fields, outnames, trans) { + var json = typeof resp == 'string' ? JSON.parse(resp) : resp; + var records = this.parseData(json.data || json, fields, outnames, trans); + return { + header: chain.header || {}, + body: records + }; + }; + /** * Some API endpoints return an object containing several arrays, representing columns of data. Each array should have * the same length, and a given array index corresponds to a single row. * @@ -8044,39 +8057,39 @@ LocusZoom.Data.Source.prototype.parseResponse = function(resp, chain, fields, ou * @param {Array} trans * @returns {Object[]} */ -LocusZoom.Data.Source.prototype.parseArraysToObjects = function(x, fields, outnames, trans) { - //intended for an object of arrays - //{"id":[1,2], "val":[5,10]} - var records = []; - fields.forEach(function(f, i) { - if (!(f in x)) {throw "field " + f + " not found in response for " + outnames[i];} - }); - // Safeguard: check that arrays are of same length - var keys = Object.keys(x); - var N = x[keys[0]].length; - var sameLength = keys.every(function(key) { - var item = x[key]; - return item.length === N; - }); - if (!sameLength) { - throw this.constructor.SOURCE_NAME + " expects a response in which all arrays of data are the same length"; - } - - for(var i = 0; i < N; i++) { - var record = {}; - for(var j=0; j1) { - if (fields.length!==2 || fields.indexOf("isrefvar")===-1) { - throw("LD does not know how to get all fields: " + fields.join(", ")); - } - } -}; - -LocusZoom.Data.LDSource.prototype.findMergeFields = function(chain) { - // since LD may be shared across sources with different namespaces - // we use regex to find columns to join on rather than - // requiring exact matches - var exactMatch = function(arr) {return function() { - var regexes = arguments; - for(var i=0; i 1) { + if (fields.length !== 2 || fields.indexOf('isrefvar') === -1) { + throw 'LD does not know how to get all fields: ' + fields.join(', '); + } } - } - return null; - };}; - var dataFields = { - id: this.params.id_field, - position: this.params.position_field, - pvalue: this.params.pvalue_field, - _names_:null - }; - if (chain && chain.body && chain.body.length>0) { - var names = Object.keys(chain.body[0]); - var nameMatch = exactMatch(names); - dataFields.id = dataFields.id || nameMatch(/\bvariant\b/) || nameMatch(/\bid\b/); - dataFields.position = dataFields.position || nameMatch(/\bposition\b/i, /\bpos\b/i); - dataFields.pvalue = dataFields.pvalue || nameMatch(/\bpvalue\b/i, /\blog_pvalue\b/i); - dataFields._names_ = names; - } - return dataFields; -}; - -LocusZoom.Data.LDSource.prototype.findRequestedFields = function(fields, outnames) { - var obj = {}; - for(var i=0; i extremeVal) { - extremeVal = x[i][pval] * sign; - extremeIdx = i; + }; + LocusZoom.Data.LDSource.prototype.findMergeFields = function (chain) { + // since LD may be shared across sources with different namespaces + // we use regex to find columns to join on rather than + // requiring exact matches + var exactMatch = function (arr) { + return function () { + var regexes = arguments; + for (var i = 0; i < regexes.length; i++) { + var regex = regexes[i]; + var m = arr.filter(function (x) { + return x.match(regex); + }); + if (m.length) { + return m[0]; + } + } + return null; + }; + }; + var dataFields = { + id: this.params.id_field, + position: this.params.position_field, + pvalue: this.params.pvalue_field, + _names_: null + }; + if (chain && chain.body && chain.body.length > 0) { + var names = Object.keys(chain.body[0]); + var nameMatch = exactMatch(names); + dataFields.id = dataFields.id || nameMatch(/\bvariant\b/) || nameMatch(/\bid\b/); + dataFields.position = dataFields.position || nameMatch(/\bposition\b/i, /\bpos\b/i); + dataFields.pvalue = dataFields.pvalue || nameMatch(/\bpvalue\b/i, /\blog_pvalue\b/i); + dataFields._names_ = names; } - } - return extremeIdx; - }; - - var refSource = state.ldrefsource || chain.header.ldrefsource || 1; - var reqFields = this.findRequestedFields(fields); - var refVar = reqFields.ldin; - if (refVar === "state") { - refVar = state.ldrefvar || chain.header.ldrefvar || "best"; - } - if (refVar === "best") { - if (!chain.body) { - throw("No association data found to find best pvalue"); - } - var keys = this.findMergeFields(chain); - if (!keys.pvalue || !keys.id) { - var columns = ""; - if (!keys.id){ columns += (columns.length ? ", " : "") + "id"; } - if (!keys.pvalue){ columns += (columns.length ? ", " : "") + "pvalue"; } - throw("Unable to find necessary column(s) for merge: " + columns + " (available: " + keys._names_ + ")"); - } - refVar = chain.body[findExtremeValue(chain.body, keys.pvalue)][keys.id]; - } - if (!chain.header) {chain.header = {};} - chain.header.ldrefvar = refVar; - return this.url + "results/?filter=reference eq " + refSource + - " and chromosome2 eq '" + state.chr + "'" + - " and position2 ge " + state.start + - " and position2 le " + state.end + - " and variant1 eq '" + refVar + "'" + - "&fields=chr,pos,rsquare"; -}; - -LocusZoom.Data.LDSource.prototype.parseResponse = function(resp, chain, fields, outnames) { - var json = JSON.parse(resp); - var keys = this.findMergeFields(chain); - var reqFields = this.findRequestedFields(fields, outnames); - if (!keys.position) { - throw("Unable to find position field for merge: " + keys._names_); - } - var leftJoin = function(left, right, lfield, rfield) { - var i=0, j=0; - while (i < left.length && j < right.position2.length) { - if (left[i][keys.position] === right.position2[j]) { - left[i][lfield] = right[rfield][j]; - i++; - j++; - } else if (left[i][keys.position] < right.position2[j]) { - i++; - } else { - j++; + return dataFields; + }; + LocusZoom.Data.LDSource.prototype.findRequestedFields = function (fields, outnames) { + var obj = {}; + for (var i = 0; i < fields.length; i++) { + if (fields[i] === 'isrefvar') { + obj.isrefvarin = fields[i]; + obj.isrefvarout = outnames && outnames[i]; + } else { + obj.ldin = fields[i]; + obj.ldout = outnames && outnames[i]; + } } - } - }; - var tagRefVariant = function(data, refvar, idfield, outname) { - for(var i=0; i extremeVal) { + extremeVal = x[i][pval] * sign; + extremeIdx = i; + } + } + return extremeIdx; + }; + var refSource = state.ldrefsource || chain.header.ldrefsource || 1; + var reqFields = this.findRequestedFields(fields); + var refVar = reqFields.ldin; + if (refVar === 'state') { + refVar = state.ldrefvar || chain.header.ldrefvar || 'best'; } - } - }; - leftJoin(chain.body, json.data, reqFields.ldout, "rsquare"); - if(reqFields.isrefvarin && chain.header.ldrefvar) { - tagRefVariant(chain.body, chain.header.ldrefvar, keys.id, reqFields.isrefvarout); - } - return chain; -}; - -/** + if (refVar === 'best') { + if (!chain.body) { + throw 'No association data found to find best pvalue'; + } + var keys = this.findMergeFields(chain); + if (!keys.pvalue || !keys.id) { + var columns = ''; + if (!keys.id) { + columns += (columns.length ? ', ' : '') + 'id'; + } + if (!keys.pvalue) { + columns += (columns.length ? ', ' : '') + 'pvalue'; + } + throw 'Unable to find necessary column(s) for merge: ' + columns + ' (available: ' + keys._names_ + ')'; + } + refVar = chain.body[findExtremeValue(chain.body, keys.pvalue)][keys.id]; + } + if (!chain.header) { + chain.header = {}; + } + chain.header.ldrefvar = refVar; + return this.url + 'results/?filter=reference eq ' + refSource + ' and chromosome2 eq \'' + state.chr + '\'' + ' and position2 ge ' + state.start + ' and position2 le ' + state.end + ' and variant1 eq \'' + refVar + '\'' + '&fields=chr,pos,rsquare'; + }; + LocusZoom.Data.LDSource.prototype.parseResponse = function (resp, chain, fields, outnames) { + var json = JSON.parse(resp); + var keys = this.findMergeFields(chain); + var reqFields = this.findRequestedFields(fields, outnames); + if (!keys.position) { + throw 'Unable to find position field for merge: ' + keys._names_; + } + var leftJoin = function (left, right, lfield, rfield) { + var i = 0, j = 0; + while (i < left.length && j < right.position2.length) { + if (left[i][keys.position] === right.position2[j]) { + left[i][lfield] = right[rfield][j]; + i++; + j++; + } else if (left[i][keys.position] < right.position2[j]) { + i++; + } else { + j++; + } + } + }; + var tagRefVariant = function (data, refvar, idfield, outname) { + for (var i = 0; i < data.length; i++) { + if (data[i][idfield] && data[i][idfield] === refvar) { + data[i][outname] = 1; + } else { + data[i][outname] = 0; + } + } + }; + leftJoin(chain.body, json.data, reqFields.ldout, 'rsquare'); + if (reqFields.isrefvarin && chain.header.ldrefvar) { + tagRefVariant(chain.body, chain.header.ldrefvar, keys.id, reqFields.isrefvarout); + } + return chain; + }; + /** * Data Source for Gene Data, as fetched from the LocusZoom API server (or compatible) * @public * @class * @augments LocusZoom.Data.Source */ -LocusZoom.Data.GeneSource = LocusZoom.Data.Source.extend(function(init) { - this.parseInit(init); -}, "GeneLZ"); - -LocusZoom.Data.GeneSource.prototype.getURL = function(state, chain, fields) { - var source = state.source || chain.header.source || this.params.source || 2; - return this.url + "?filter=source in " + source + - " and chrom eq '" + state.chr + "'" + - " and start le " + state.end + - " and end ge " + state.start; -}; - -LocusZoom.Data.GeneSource.prototype.parseResponse = function(resp, chain, fields, outnames) { - var json = JSON.parse(resp); - return {header: chain.header, body: json.data}; -}; - -/** + LocusZoom.Data.GeneSource = LocusZoom.Data.Source.extend(function (init) { + this.parseInit(init); + }, 'GeneLZ'); + LocusZoom.Data.GeneSource.prototype.getURL = function (state, chain, fields) { + var source = state.source || chain.header.source || this.params.source || 2; + return this.url + '?filter=source in ' + source + ' and chrom eq \'' + state.chr + '\'' + ' and start le ' + state.end + ' and end ge ' + state.start; + }; + LocusZoom.Data.GeneSource.prototype.parseResponse = function (resp, chain, fields, outnames) { + var json = JSON.parse(resp); + return { + header: chain.header, + body: json.data + }; + }; + /** * Data Source for Gene Constraint Data, as fetched from the LocusZoom API server (or compatible) * @public * @class * @augments LocusZoom.Data.Source */ -LocusZoom.Data.GeneConstraintSource = LocusZoom.Data.Source.extend(function(init) { - this.parseInit(init); -}, "GeneConstraintLZ"); - -LocusZoom.Data.GeneConstraintSource.prototype.getURL = function() { - return this.url; -}; - -LocusZoom.Data.GeneConstraintSource.prototype.getCacheKey = function(state, chain, fields) { - return this.url + JSON.stringify(state); -}; - -LocusZoom.Data.GeneConstraintSource.prototype.fetchRequest = function(state, chain, fields) { - var geneids = []; - chain.body.forEach(function(gene){ - var gene_id = gene.gene_id; - if (gene_id.indexOf(".")){ - gene_id = gene_id.substr(0, gene_id.indexOf(".")); - } - geneids.push(gene_id); - }); - var url = this.getURL(state, chain, fields); - var body = "geneids=" + encodeURIComponent(JSON.stringify(geneids)); - var headers = { - "Content-Type": "application/x-www-form-urlencoded" - }; - return LocusZoom.createCORSPromise("POST", url, body, headers); -}; - -LocusZoom.Data.GeneConstraintSource.prototype.parseResponse = function(resp, chain, fields, outnames) { - if (!resp){ - return { header: chain.header, body: chain.body }; - } - var data = JSON.parse(resp); - // Loop through the array of genes in the body and match each to a result from the constraints request - var constraint_fields = ["bp", "exp_lof", "exp_mis", "exp_syn", "lof_z", "mis_z", "mu_lof", "mu_mis","mu_syn", "n_exons", "n_lof", "n_mis", "n_syn", "pLI", "syn_z"]; - chain.body.forEach(function(gene, i){ - var gene_id = gene.gene_id; - if (gene_id.indexOf(".")){ - gene_id = gene_id.substr(0, gene_id.indexOf(".")); - } - constraint_fields.forEach(function(field){ - // Do not overwrite any fields defined in the original gene source - if (typeof chain.body[i][field] != "undefined"){ return; } - if (data[gene_id]){ - var val = data[gene_id][field]; - if (typeof val == "number" && val.toString().indexOf(".") !== -1){ - val = parseFloat(val.toFixed(2)); - } - chain.body[i][field] = val; - } else { - // If the gene did not come back in the response then set the same field with a null values - chain.body[i][field] = null; + LocusZoom.Data.GeneConstraintSource = LocusZoom.Data.Source.extend(function (init) { + this.parseInit(init); + }, 'GeneConstraintLZ'); + LocusZoom.Data.GeneConstraintSource.prototype.getURL = function () { + return this.url; + }; + LocusZoom.Data.GeneConstraintSource.prototype.getCacheKey = function (state, chain, fields) { + return this.url + JSON.stringify(state); + }; + LocusZoom.Data.GeneConstraintSource.prototype.fetchRequest = function (state, chain, fields) { + var geneids = []; + chain.body.forEach(function (gene) { + var gene_id = gene.gene_id; + if (gene_id.indexOf('.')) { + gene_id = gene_id.substr(0, gene_id.indexOf('.')); + } + geneids.push(gene_id); + }); + var url = this.getURL(state, chain, fields); + var body = 'geneids=' + encodeURIComponent(JSON.stringify(geneids)); + var headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; + return LocusZoom.createCORSPromise('POST', url, body, headers); + }; + LocusZoom.Data.GeneConstraintSource.prototype.parseResponse = function (resp, chain, fields, outnames) { + if (!resp) { + return { + header: chain.header, + body: chain.body + }; } - }); - }); - return { header: chain.header, body: chain.body }; -}; - -/** + var data = JSON.parse(resp); + // Loop through the array of genes in the body and match each to a result from the constraints request + var constraint_fields = [ + 'bp', + 'exp_lof', + 'exp_mis', + 'exp_syn', + 'lof_z', + 'mis_z', + 'mu_lof', + 'mu_mis', + 'mu_syn', + 'n_exons', + 'n_lof', + 'n_mis', + 'n_syn', + 'pLI', + 'syn_z' + ]; + chain.body.forEach(function (gene, i) { + var gene_id = gene.gene_id; + if (gene_id.indexOf('.')) { + gene_id = gene_id.substr(0, gene_id.indexOf('.')); + } + constraint_fields.forEach(function (field) { + // Do not overwrite any fields defined in the original gene source + if (typeof chain.body[i][field] != 'undefined') { + return; + } + if (data[gene_id]) { + var val = data[gene_id][field]; + if (typeof val == 'number' && val.toString().indexOf('.') !== -1) { + val = parseFloat(val.toFixed(2)); + } + chain.body[i][field] = val; + } else { + // If the gene did not come back in the response then set the same field with a null values + chain.body[i][field] = null; + } + }); + }); + return { + header: chain.header, + body: chain.body + }; + }; + /** * Data Source for Recombination Rate Data, as fetched from the LocusZoom API server (or compatible) * @public * @class * @augments LocusZoom.Data.Source */ -LocusZoom.Data.RecombinationRateSource = LocusZoom.Data.Source.extend(function(init) { - this.parseInit(init); -}, "RecombLZ"); - -LocusZoom.Data.RecombinationRateSource.prototype.getURL = function(state, chain, fields) { - var source = state.recombsource || chain.header.recombsource || this.params.source || 15; - return this.url + "?filter=id in " + source + - " and chromosome eq '" + state.chr + "'" + - " and position le " + state.end + - " and position ge " + state.start; -}; - -/** + LocusZoom.Data.RecombinationRateSource = LocusZoom.Data.Source.extend(function (init) { + this.parseInit(init); + }, 'RecombLZ'); + LocusZoom.Data.RecombinationRateSource.prototype.getURL = function (state, chain, fields) { + var source = state.recombsource || chain.header.recombsource || this.params.source || 15; + return this.url + '?filter=id in ' + source + ' and chromosome eq \'' + state.chr + '\'' + ' and position le ' + state.end + ' and position ge ' + state.start; + }; + /** * Data Source for Interval Annotation Data (e.g. BED Tracks), as fetched from the LocusZoom API server (or compatible) * @public * @class * @augments LocusZoom.Data.Source */ -LocusZoom.Data.IntervalSource = LocusZoom.Data.Source.extend(function(init) { - this.parseInit(init); -}, "IntervalLZ"); - -LocusZoom.Data.IntervalSource.prototype.getURL = function(state, chain, fields) { - var source = state.bedtracksource || chain.header.bedtracksource || this.params.source || 16; - return this.url + "?filter=id in " + source + - " and chromosome eq '" + state.chr + "'" + - " and start le " + state.end + - " and end ge " + state.start; -}; - -/** + LocusZoom.Data.IntervalSource = LocusZoom.Data.Source.extend(function (init) { + this.parseInit(init); + }, 'IntervalLZ'); + LocusZoom.Data.IntervalSource.prototype.getURL = function (state, chain, fields) { + var source = state.bedtracksource || chain.header.bedtracksource || this.params.source || 16; + return this.url + '?filter=id in ' + source + ' and chromosome eq \'' + state.chr + '\'' + ' and start le ' + state.end + ' and end ge ' + state.start; + }; + /** * Data Source for static blobs of JSON Data. This does not perform additional parsing, and therefore it is the * responsibility of the user to pass information in a format that can be read and understood by the chosen plot. * @public * @class * @augments LocusZoom.Data.Source */ -LocusZoom.Data.StaticSource = LocusZoom.Data.Source.extend(function(data) { - /** @member {Object} */ - this._data = data; -},"StaticJSON"); - -LocusZoom.Data.StaticSource.prototype.getRequest = function(state, chain, fields) { - return Q.fcall(function() {return this._data;}.bind(this)); -}; - -LocusZoom.Data.StaticSource.prototype.toJSON = function() { - return [Object.getPrototypeOf(this).constructor.SOURCE_NAME, this._data]; -}; - -/** + LocusZoom.Data.StaticSource = LocusZoom.Data.Source.extend(function (data) { + /** @member {Object} */ + this._data = data; + }, 'StaticJSON'); + LocusZoom.Data.StaticSource.prototype.getRequest = function (state, chain, fields) { + return Q.fcall(function () { + return this._data; + }.bind(this)); + }; + LocusZoom.Data.StaticSource.prototype.toJSON = function () { + return [ + Object.getPrototypeOf(this).constructor.SOURCE_NAME, + this._data + ]; + }; + /** * Data source for PheWAS data served from external JSON files * @public * @class @@ -8519,26 +8542,32 @@ LocusZoom.Data.StaticSource.prototype.toJSON = function() { * @param {String[]} init.build This datasource expects to be provided the name of the genome build that will be used to * provide pheWAS results for this position. Note positions may not translate between builds. */ -LocusZoom.Data.PheWASSource = LocusZoom.Data.Source.extend(function(init) { - this.parseInit(init); -}, "PheWASLZ"); -LocusZoom.Data.PheWASSource.prototype.getURL = function(state, chain, fields) { - var build = this.params.build; - if (!build || !Array.isArray(build) || !build.length) { - throw ["Data source", this.constructor.SOURCE_NAME, "requires that you specify array of one or more desired genome build names"].join(" "); - } - var url = [ - this.url, - "?filter=variant eq '", encodeURIComponent(state.variant), "'&format=objects&", - build.map(function(item) {return "build=" + encodeURIComponent(item);}).join("&") - ]; - return url.join(""); -}; - -/* global LocusZoom */ -"use strict"; - -/** + LocusZoom.Data.PheWASSource = LocusZoom.Data.Source.extend(function (init) { + this.parseInit(init); + }, 'PheWASLZ'); + LocusZoom.Data.PheWASSource.prototype.getURL = function (state, chain, fields) { + var build = this.params.build; + if (!build || !Array.isArray(build) || !build.length) { + throw [ + 'Data source', + this.constructor.SOURCE_NAME, + 'requires that you specify array of one or more desired genome build names' + ].join(' '); + } + var url = [ + this.url, + '?filter=variant eq \'', + encodeURIComponent(state.variant), + '\'&format=objects&', + build.map(function (item) { + return 'build=' + encodeURIComponent(item); + }).join('&') + ]; + return url.join(''); + }; + /* global LocusZoom */ + 'use strict'; + /** * An independent LocusZoom object that renders a unique set of data and subpanels. * Many such LocusZoom objects can exist simultaneously on a single page, each having its own layout. * @@ -8551,111 +8580,98 @@ LocusZoom.Data.PheWASSource.prototype.getURL = function(state, chain, fields) { * @param {LocusZoom.DataSources} datasource Ensemble of data providers used by the plot * @param {Object} layout A JSON-serializable object of layout configuration parameters */ -LocusZoom.Plot = function(id, datasource, layout) { - /** @member Boolean} */ - this.initialized = false; - // TODO: This makes sense for all other locuszoom elements to have; determine whether this is interface boilerplate or something that can be removed - this.parent_plot = this; - - /** @member {String} */ - this.id = id; - - /** @member {Element} */ - this.container = null; - /** + LocusZoom.Plot = function (id, datasource, layout) { + /** @member Boolean} */ + this.initialized = false; + // TODO: This makes sense for all other locuszoom elements to have; determine whether this is interface boilerplate or something that can be removed + this.parent_plot = this; + /** @member {String} */ + this.id = id; + /** @member {Element} */ + this.container = null; + /** * Selector for a node that will contain the plot. (set externally by populate methods) * @member {d3.selection} */ - this.svg = null; - - /** @member {Object.} */ - this.panels = {}; - /** + this.svg = null; + /** @member {Object.} */ + this.panels = {}; + /** * TODO: This is currently used by external classes that manipulate the parent and may indicate room for a helper method in the api to coordinate boilerplate * @protected * @member {String[]} */ - this.panel_ids_by_y_index = []; - - /** + this.panel_ids_by_y_index = []; + /** * Notify each child panel of the plot of changes in panel ordering/ arrangement */ - this.applyPanelYIndexesToPanelLayouts = function(){ - this.panel_ids_by_y_index.forEach(function(pid, idx){ - this.panels[pid].layout.y_index = idx; - }.bind(this)); - }; - - /** + this.applyPanelYIndexesToPanelLayouts = function () { + this.panel_ids_by_y_index.forEach(function (pid, idx) { + this.panels[pid].layout.y_index = idx; + }.bind(this)); + }; + /** * Get the qualified ID pathname for the plot * @returns {String} */ - this.getBaseId = function(){ - return this.id; - }; - - /** + this.getBaseId = function () { + return this.id; + }; + /** * Track update operations (reMap) performed on all child panels, and notify the parent plot when complete * TODO: Reconsider whether we need to be tracking this as global state outside of context of specific operations * @protected * @member {Promise[]} */ - this.remap_promises = []; - - if (typeof layout == "undefined"){ - /** + this.remap_promises = []; + if (typeof layout == 'undefined') { + /** * The layout is a serializable object used to describe the composition of the Plot * If no layout was passed, use the Standard Association Layout * Otherwise merge whatever was passed with the Default Layout * TODO: Review description; we *always* merge with default layout? * @member {Object} */ - this.layout = LocusZoom.Layouts.merge({}, LocusZoom.Layouts.get("plot", "standard_association")); - } else { - this.layout = layout; - } - LocusZoom.Layouts.merge(this.layout, LocusZoom.Plot.DefaultLayout); - - /** + this.layout = LocusZoom.Layouts.merge({}, LocusZoom.Layouts.get('plot', 'standard_association')); + } else { + this.layout = layout; + } + LocusZoom.Layouts.merge(this.layout, LocusZoom.Plot.DefaultLayout); + /** * Values in the layout object may change during rendering etc. Retain a copy of the original plot state * @member {Object} */ - this._base_layout = JSON.parse(JSON.stringify(this.layout)); - - - /** + this._base_layout = JSON.parse(JSON.stringify(this.layout)); + /** * Create a shortcut to the state in the layout on the Plot. Tracking in the layout allows the plot to be created * with initial state/setup. * * Tracks state of the plot, eg start and end position * @member {Object} */ - this.state = this.layout.state; - - /** @member {LocusZoom.Data.Requester} */ - this.lzd = new LocusZoom.Data.Requester(datasource); - - /** + this.state = this.layout.state; + /** @member {LocusZoom.Data.Requester} */ + this.lzd = new LocusZoom.Data.Requester(datasource); + /** * Window.onresize listener (responsive layouts only) * TODO: .on appears to return a selection, not a listener? Check logic here * https://github.com/d3/d3-selection/blob/00b904b9bcec4dfaf154ae0bbc777b1fc1d7bc08/test/selection/on-test.js#L11 * @deprecated * @member {d3.selection} */ - this.window_onresize = null; - - /** + this.window_onresize = null; + /** * Known event hooks that the panel can respond to * @protected * @member {Object} */ - this.event_hooks = { - "layout_changed": [], - "data_requested": [], - "data_rendered": [], - "element_clicked": [] - }; - /** + this.event_hooks = { + 'layout_changed': [], + 'data_requested': [], + 'data_rendered': [], + 'element_clicked': [] + }; + /** * There are several events that a LocusZoom plot can "emit" when appropriate, and LocusZoom supports registering * "hooks" for these events which are essentially custom functions intended to fire at certain times. * @@ -8677,196 +8693,183 @@ LocusZoom.Plot = function(id, datasource, layout) { * @param {function} hook * @returns {LocusZoom.Plot} */ - this.on = function(event, hook){ - if (typeof "event" != "string" || !Array.isArray(this.event_hooks[event])){ - throw("Unable to register event hook, invalid event: " + event.toString()); - } - if (typeof hook != "function"){ - throw("Unable to register event hook, invalid hook function passed"); - } - this.event_hooks[event].push(hook); - return this; - }; - /** + this.on = function (event, hook) { + if (typeof 'event' != 'string' || !Array.isArray(this.event_hooks[event])) { + throw 'Unable to register event hook, invalid event: ' + event.toString(); + } + if (typeof hook != 'function') { + throw 'Unable to register event hook, invalid hook function passed'; + } + this.event_hooks[event].push(hook); + return this; + }; + /** * Handle running of event hooks when an event is emitted * @protected * @param {string} event A known event name * @param {*} context Controls function execution context (value of `this` for the hook to be fired) * @returns {LocusZoom.Plot} */ - this.emit = function(event, context){ - if (typeof "event" != "string" || !Array.isArray(this.event_hooks[event])){ - throw("LocusZoom attempted to throw an invalid event: " + event.toString()); - } - context = context || this; - this.event_hooks[event].forEach(function(hookToRun) { - hookToRun.call(context); - }); - return this; - }; - - /** + this.emit = function (event, context) { + if (typeof 'event' != 'string' || !Array.isArray(this.event_hooks[event])) { + throw 'LocusZoom attempted to throw an invalid event: ' + event.toString(); + } + context = context || this; + this.event_hooks[event].forEach(function (hookToRun) { + hookToRun.call(context); + }); + return this; + }; + /** * Get an object with the x and y coordinates of the plot's origin in terms of the entire page * Necessary for positioning any HTML elements over the plot * @returns {{x: Number, y: Number, width: Number, height: Number}} */ - this.getPageOrigin = function(){ - var bounding_client_rect = this.svg.node().getBoundingClientRect(); - var x_offset = document.documentElement.scrollLeft || document.body.scrollLeft; - var y_offset = document.documentElement.scrollTop || document.body.scrollTop; - var container = this.svg.node(); - while (container.parentNode !== null){ - container = container.parentNode; - if (container !== document && d3.select(container).style("position") !== "static"){ - x_offset = -1 * container.getBoundingClientRect().left; - y_offset = -1 * container.getBoundingClientRect().top; - break; - } - } - return { - x: x_offset + bounding_client_rect.left, - y: y_offset + bounding_client_rect.top, - width: bounding_client_rect.width, - height: bounding_client_rect.height - }; - }; - - /** + this.getPageOrigin = function () { + var bounding_client_rect = this.svg.node().getBoundingClientRect(); + var x_offset = document.documentElement.scrollLeft || document.body.scrollLeft; + var y_offset = document.documentElement.scrollTop || document.body.scrollTop; + var container = this.svg.node(); + while (container.parentNode !== null) { + container = container.parentNode; + if (container !== document && d3.select(container).style('position') !== 'static') { + x_offset = -1 * container.getBoundingClientRect().left; + y_offset = -1 * container.getBoundingClientRect().top; + break; + } + } + return { + x: x_offset + bounding_client_rect.left, + y: y_offset + bounding_client_rect.top, + width: bounding_client_rect.width, + height: bounding_client_rect.height + }; + }; + /** * Get the top and left offset values for the plot's container element (the div that was populated) * @returns {{top: number, left: number}} */ - this.getContainerOffset = function(){ - var offset = { top: 0, left: 0 }; - var container = this.container.offsetParent || null; - while (container !== null){ - offset.top += container.offsetTop; - offset.left += container.offsetLeft; - container = container.offsetParent || null; - } - return offset; - }; - - // - /** + this.getContainerOffset = function () { + var offset = { + top: 0, + left: 0 + }; + var container = this.container.offsetParent || null; + while (container !== null) { + offset.top += container.offsetTop; + offset.left += container.offsetLeft; + container = container.offsetParent || null; + } + return offset; + }; + // + /** * Event information describing interaction (e.g. panning and zooming) is stored on the plot * TODO: Add/ document details of interaction structure as we expand * @member {{panel_id: String, linked_panel_ids: Array, x_linked: *, dragging: *, zooming: *}} * @returns {LocusZoom.Plot} */ - this.interaction = {}; - - /** + this.interaction = {}; + /** * Track whether the target panel can respond to mouse interaction events * @param {String} panel_id * @returns {boolean} */ - this.canInteract = function(panel_id){ - panel_id = panel_id || null; - if (panel_id){ - return ((typeof this.interaction.panel_id == "undefined" || this.interaction.panel_id === panel_id) && !this.loading_data); - } else { - return !(this.interaction.dragging || this.interaction.zooming || this.loading_data); - } - }; - - // Initialize the layout - this.initializeLayout(); - // TODO: Possibly superfluous return from constructor - return this; -}; - -/** + this.canInteract = function (panel_id) { + panel_id = panel_id || null; + if (panel_id) { + return (typeof this.interaction.panel_id == 'undefined' || this.interaction.panel_id === panel_id) && !this.loading_data; + } else { + return !(this.interaction.dragging || this.interaction.zooming || this.loading_data); + } + }; + // Initialize the layout + this.initializeLayout(); + // TODO: Possibly superfluous return from constructor + return this; + }; + /** * Default/ expected configuration parameters for basic plotting; most plots will override * * @protected * @static * @type {Object} */ -LocusZoom.Plot.DefaultLayout = { - state: {}, - width: 1, - height: 1, - min_width: 1, - min_height: 1, - responsive_resize: false, - aspect_ratio: 1, - panels: [], - dashboard: { - components: [] - }, - panel_boundaries: true, - mouse_guide: true -}; - -/** + LocusZoom.Plot.DefaultLayout = { + state: {}, + width: 1, + height: 1, + min_width: 1, + min_height: 1, + responsive_resize: false, + aspect_ratio: 1, + panels: [], + dashboard: { components: [] }, + panel_boundaries: true, + mouse_guide: true + }; + /** * Helper method to sum the proportional dimensions of panels, a value that's checked often as panels are added/removed * @param {('Height'|'Width')} dimension * @returns {number} */ -LocusZoom.Plot.prototype.sumProportional = function(dimension){ - if (dimension !== "height" && dimension !== "width"){ - throw ("Bad dimension value passed to LocusZoom.Plot.prototype.sumProportional"); - } - var total = 0; - for (var id in this.panels){ - // Ensure every panel contributing to the sum has a non-zero proportional dimension - if (!this.panels[id].layout["proportional_" + dimension]){ - this.panels[id].layout["proportional_" + dimension] = 1 / Object.keys(this.panels).length; - } - total += this.panels[id].layout["proportional_" + dimension]; - } - return total; -}; - -/** + LocusZoom.Plot.prototype.sumProportional = function (dimension) { + if (dimension !== 'height' && dimension !== 'width') { + throw 'Bad dimension value passed to LocusZoom.Plot.prototype.sumProportional'; + } + var total = 0; + for (var id in this.panels) { + // Ensure every panel contributing to the sum has a non-zero proportional dimension + if (!this.panels[id].layout['proportional_' + dimension]) { + this.panels[id].layout['proportional_' + dimension] = 1 / Object.keys(this.panels).length; + } + total += this.panels[id].layout['proportional_' + dimension]; + } + return total; + }; + /** * Resize the plot to fit the bounding container * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.rescaleSVG = function(){ - var clientRect = this.svg.node().getBoundingClientRect(); - this.setDimensions(clientRect.width, clientRect.height); - return this; -}; - -/** + LocusZoom.Plot.prototype.rescaleSVG = function () { + var clientRect = this.svg.node().getBoundingClientRect(); + this.setDimensions(clientRect.width, clientRect.height); + return this; + }; + /** * Prepare the plot for first use by performing parameter validation, setting up panels, and calculating dimensions * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.initializeLayout = function(){ - - // Sanity check layout values - // TODO: Find a way to generally abstract this, maybe into an object that models allowed layout values? - if (isNaN(this.layout.width) || this.layout.width <= 0){ - throw ("Plot layout parameter `width` must be a positive number"); - } - if (isNaN(this.layout.height) || this.layout.height <= 0){ - throw ("Plot layout parameter `width` must be a positive number"); - } - if (isNaN(this.layout.aspect_ratio) || this.layout.aspect_ratio <= 0){ - throw ("Plot layout parameter `aspect_ratio` must be a positive number"); - } - - // If this is a responsive layout then set a namespaced/unique onresize event listener on the window - if (this.layout.responsive_resize){ - this.window_onresize = d3.select(window).on("resize.lz-"+this.id, function(){ - this.rescaleSVG(); - }.bind(this)); - // Forcing one additional setDimensions() call after the page is loaded clears up - // any disagreements between the initial layout and the loaded responsive container's size - d3.select(window).on("load.lz-"+this.id, function(){ - this.setDimensions(); - }.bind(this)); - } - - // Add panels - this.layout.panels.forEach(function(panel_layout){ - this.addPanel(panel_layout); - }.bind(this)); - - return this; -}; - -/** + LocusZoom.Plot.prototype.initializeLayout = function () { + // Sanity check layout values + // TODO: Find a way to generally abstract this, maybe into an object that models allowed layout values? + if (isNaN(this.layout.width) || this.layout.width <= 0) { + throw 'Plot layout parameter `width` must be a positive number'; + } + if (isNaN(this.layout.height) || this.layout.height <= 0) { + throw 'Plot layout parameter `width` must be a positive number'; + } + if (isNaN(this.layout.aspect_ratio) || this.layout.aspect_ratio <= 0) { + throw 'Plot layout parameter `aspect_ratio` must be a positive number'; + } + // If this is a responsive layout then set a namespaced/unique onresize event listener on the window + if (this.layout.responsive_resize) { + this.window_onresize = d3.select(window).on('resize.lz-' + this.id, function () { + this.rescaleSVG(); + }.bind(this)); + // Forcing one additional setDimensions() call after the page is loaded clears up + // any disagreements between the initial layout and the loaded responsive container's size + d3.select(window).on('load.lz-' + this.id, function () { + this.setDimensions(); + }.bind(this)); + } + // Add panels + this.layout.panels.forEach(function (panel_layout) { + this.addPanel(panel_layout); + }.bind(this)); + return this; + }; + /** * Set the dimensions for a plot, and ensure that panels are sized and positioned correctly. * * If dimensions are provided, resizes each panel proportionally to match the new plot dimensions. Otherwise, @@ -8874,155 +8877,135 @@ LocusZoom.Plot.prototype.initializeLayout = function(){ * @param {Number} [width] If provided and larger than minimum size, set plot to this width * @param {Number} [height] If provided and larger than minimum size, set plot to this height * @returns {LocusZoom.Plot} - */ -LocusZoom.Plot.prototype.setDimensions = function(width, height){ - - var id; - - // Update minimum allowable width and height by aggregating minimums from panels, then apply minimums to containing element. - var min_width = parseFloat(this.layout.min_width) || 0; - var min_height = parseFloat(this.layout.min_height) || 0; - for (id in this.panels){ - min_width = Math.max(min_width, this.panels[id].layout.min_width); - if (parseFloat(this.panels[id].layout.min_height) > 0 && parseFloat(this.panels[id].layout.proportional_height) > 0){ - min_height = Math.max(min_height, (this.panels[id].layout.min_height / this.panels[id].layout.proportional_height)); - } - } - this.layout.min_width = Math.max(min_width, 1); - this.layout.min_height = Math.max(min_height, 1); - d3.select(this.svg.node().parentNode).style({ - "min-width": this.layout.min_width + "px", - "min-height": this.layout.min_height + "px" - }); - - // If width and height arguments were passed then adjust them against plot minimums if necessary. - // Then resize the plot and proportionally resize panels to fit inside the new plot dimensions. - if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0){ - this.layout.width = Math.max(Math.round(+width), this.layout.min_width); - this.layout.height = Math.max(Math.round(+height), this.layout.min_height); - this.layout.aspect_ratio = this.layout.width / this.layout.height; - // Override discrete values if resizing responsively - if (this.layout.responsive_resize){ - if (this.svg){ - this.layout.width = Math.max(this.svg.node().parentNode.getBoundingClientRect().width, this.layout.min_width); - } - this.layout.height = this.layout.width / this.layout.aspect_ratio; - if (this.layout.height < this.layout.min_height){ - this.layout.height = this.layout.min_height; - this.layout.width = this.layout.height * this.layout.aspect_ratio; + */ + LocusZoom.Plot.prototype.setDimensions = function (width, height) { + var id; + // Update minimum allowable width and height by aggregating minimums from panels, then apply minimums to containing element. + var min_width = parseFloat(this.layout.min_width) || 0; + var min_height = parseFloat(this.layout.min_height) || 0; + for (id in this.panels) { + min_width = Math.max(min_width, this.panels[id].layout.min_width); + if (parseFloat(this.panels[id].layout.min_height) > 0 && parseFloat(this.panels[id].layout.proportional_height) > 0) { + min_height = Math.max(min_height, this.panels[id].layout.min_height / this.panels[id].layout.proportional_height); + } } - } - // Resize/reposition panels to fit, update proportional origins if necessary - var y_offset = 0; - this.panel_ids_by_y_index.forEach(function(panel_id){ - var panel_width = this.layout.width; - var panel_height = this.panels[panel_id].layout.proportional_height * this.layout.height; - this.panels[panel_id].setDimensions(panel_width, panel_height); - this.panels[panel_id].setOrigin(0, y_offset); - this.panels[panel_id].layout.proportional_origin.x = 0; - this.panels[panel_id].layout.proportional_origin.y = y_offset / this.layout.height; - y_offset += panel_height; - this.panels[panel_id].dashboard.update(); - }.bind(this)); - } - - // If width and height arguments were NOT passed (and panels exist) then determine the plot dimensions - // by making it conform to panel dimensions, assuming panels are already positioned correctly. - else if (Object.keys(this.panels).length) { - this.layout.width = 0; - this.layout.height = 0; - for (id in this.panels){ - this.layout.width = Math.max(this.panels[id].layout.width, this.layout.width); - this.layout.height += this.panels[id].layout.height; - } - this.layout.width = Math.max(this.layout.width, this.layout.min_width); - this.layout.height = Math.max(this.layout.height, this.layout.min_height); - } - - // Keep aspect ratio in agreement with dimensions - this.layout.aspect_ratio = this.layout.width / this.layout.height; - - // Apply layout width and height as discrete values or viewbox values - if (this.svg !== null){ - if (this.layout.responsive_resize){ - this.svg - .attr("viewBox", "0 0 " + this.layout.width + " " + this.layout.height) - .attr("preserveAspectRatio", "xMinYMin meet"); - } else { - this.svg.attr("width", this.layout.width).attr("height", this.layout.height); - } - } - - // If the plot has been initialized then trigger some necessary render functions - if (this.initialized){ - this.panel_boundaries.position(); - this.dashboard.update(); - this.curtain.update(); - this.loader.update(); - } - - return this.emit("layout_changed"); -}; - -/** + this.layout.min_width = Math.max(min_width, 1); + this.layout.min_height = Math.max(min_height, 1); + d3.select(this.svg.node().parentNode).style({ + 'min-width': this.layout.min_width + 'px', + 'min-height': this.layout.min_height + 'px' + }); + // If width and height arguments were passed then adjust them against plot minimums if necessary. + // Then resize the plot and proportionally resize panels to fit inside the new plot dimensions. + if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0) { + this.layout.width = Math.max(Math.round(+width), this.layout.min_width); + this.layout.height = Math.max(Math.round(+height), this.layout.min_height); + this.layout.aspect_ratio = this.layout.width / this.layout.height; + // Override discrete values if resizing responsively + if (this.layout.responsive_resize) { + if (this.svg) { + this.layout.width = Math.max(this.svg.node().parentNode.getBoundingClientRect().width, this.layout.min_width); + } + this.layout.height = this.layout.width / this.layout.aspect_ratio; + if (this.layout.height < this.layout.min_height) { + this.layout.height = this.layout.min_height; + this.layout.width = this.layout.height * this.layout.aspect_ratio; + } + } + // Resize/reposition panels to fit, update proportional origins if necessary + var y_offset = 0; + this.panel_ids_by_y_index.forEach(function (panel_id) { + var panel_width = this.layout.width; + var panel_height = this.panels[panel_id].layout.proportional_height * this.layout.height; + this.panels[panel_id].setDimensions(panel_width, panel_height); + this.panels[panel_id].setOrigin(0, y_offset); + this.panels[panel_id].layout.proportional_origin.x = 0; + this.panels[panel_id].layout.proportional_origin.y = y_offset / this.layout.height; + y_offset += panel_height; + this.panels[panel_id].dashboard.update(); + }.bind(this)); + } // If width and height arguments were NOT passed (and panels exist) then determine the plot dimensions + // by making it conform to panel dimensions, assuming panels are already positioned correctly. + else if (Object.keys(this.panels).length) { + this.layout.width = 0; + this.layout.height = 0; + for (id in this.panels) { + this.layout.width = Math.max(this.panels[id].layout.width, this.layout.width); + this.layout.height += this.panels[id].layout.height; + } + this.layout.width = Math.max(this.layout.width, this.layout.min_width); + this.layout.height = Math.max(this.layout.height, this.layout.min_height); + } + // Keep aspect ratio in agreement with dimensions + this.layout.aspect_ratio = this.layout.width / this.layout.height; + // Apply layout width and height as discrete values or viewbox values + if (this.svg !== null) { + if (this.layout.responsive_resize) { + this.svg.attr('viewBox', '0 0 ' + this.layout.width + ' ' + this.layout.height).attr('preserveAspectRatio', 'xMinYMin meet'); + } else { + this.svg.attr('width', this.layout.width).attr('height', this.layout.height); + } + } + // If the plot has been initialized then trigger some necessary render functions + if (this.initialized) { + this.panel_boundaries.position(); + this.dashboard.update(); + this.curtain.update(); + this.loader.update(); + } + return this.emit('layout_changed'); + }; + /** * Create a new panel from a layout, and handle the work of initializing and placing the panel on the plot * @param {Object} layout * @returns {LocusZoom.Panel} */ -LocusZoom.Plot.prototype.addPanel = function(layout){ - - // Sanity checks - if (typeof layout !== "object"){ - throw "Invalid panel layout passed to LocusZoom.Plot.prototype.addPanel()"; - } - - // Create the Panel and set its parent - var panel = new LocusZoom.Panel(layout, this); - - // Store the Panel on the Plot - this.panels[panel.id] = panel; - - // If a discrete y_index was set in the layout then adjust other panel y_index values to accommodate this one - if (panel.layout.y_index !== null && !isNaN(panel.layout.y_index) - && this.panel_ids_by_y_index.length > 0){ - // Negative y_index values should count backwards from the end, so convert negatives to appropriate values here - if (panel.layout.y_index < 0){ - panel.layout.y_index = Math.max(this.panel_ids_by_y_index.length + panel.layout.y_index, 0); - } - this.panel_ids_by_y_index.splice(panel.layout.y_index, 0, panel.id); - this.applyPanelYIndexesToPanelLayouts(); - } else { - var length = this.panel_ids_by_y_index.push(panel.id); - this.panels[panel.id].layout.y_index = length - 1; - } - - // Determine if this panel was already in the layout.panels array. - // If it wasn't, add it. Either way store the layout.panels array index on the panel. - var layout_idx = null; - this.layout.panels.forEach(function(panel_layout, idx){ - if (panel_layout.id === panel.id){ layout_idx = idx; } - }); - if (layout_idx === null){ - layout_idx = this.layout.panels.push(this.panels[panel.id].layout) - 1; - } - this.panels[panel.id].layout_idx = layout_idx; - - // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space - if (this.initialized){ - this.positionPanels(); - // Initialize and load data into the new panel - this.panels[panel.id].initialize(); - this.panels[panel.id].reMap(); - // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip - // positioning. TODO: make this additional call unnecessary. - this.setDimensions(this.layout.width, this.layout.height); - } - - return this.panels[panel.id]; -}; - - -/** + LocusZoom.Plot.prototype.addPanel = function (layout) { + // Sanity checks + if (typeof layout !== 'object') { + throw 'Invalid panel layout passed to LocusZoom.Plot.prototype.addPanel()'; + } + // Create the Panel and set its parent + var panel = new LocusZoom.Panel(layout, this); + // Store the Panel on the Plot + this.panels[panel.id] = panel; + // If a discrete y_index was set in the layout then adjust other panel y_index values to accommodate this one + if (panel.layout.y_index !== null && !isNaN(panel.layout.y_index) && this.panel_ids_by_y_index.length > 0) { + // Negative y_index values should count backwards from the end, so convert negatives to appropriate values here + if (panel.layout.y_index < 0) { + panel.layout.y_index = Math.max(this.panel_ids_by_y_index.length + panel.layout.y_index, 0); + } + this.panel_ids_by_y_index.splice(panel.layout.y_index, 0, panel.id); + this.applyPanelYIndexesToPanelLayouts(); + } else { + var length = this.panel_ids_by_y_index.push(panel.id); + this.panels[panel.id].layout.y_index = length - 1; + } + // Determine if this panel was already in the layout.panels array. + // If it wasn't, add it. Either way store the layout.panels array index on the panel. + var layout_idx = null; + this.layout.panels.forEach(function (panel_layout, idx) { + if (panel_layout.id === panel.id) { + layout_idx = idx; + } + }); + if (layout_idx === null) { + layout_idx = this.layout.panels.push(this.panels[panel.id].layout) - 1; + } + this.panels[panel.id].layout_idx = layout_idx; + // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space + if (this.initialized) { + this.positionPanels(); + // Initialize and load data into the new panel + this.panels[panel.id].initialize(); + this.panels[panel.id].reMap(); + // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip + // positioning. TODO: make this additional call unnecessary. + this.setDimensions(this.layout.width, this.layout.height); + } + return this.panels[panel.id]; + }; + /** * Clear all state, tooltips, and other persisted data associated with one (or all) panel(s) in the plot * * This is useful when reloading an existing plot with new data, eg "click for genome region" links. @@ -9032,88 +9015,73 @@ LocusZoom.Plot.prototype.addPanel = function(layout){ * and is useful for when the panel is being removed; `reset` is best when the panel will be reused in place. * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.clearPanelData = function(panelId, mode) { - mode = mode || "wipe"; - - // TODO: Add unit tests for this method - var panelsList; - if (panelId) { - panelsList = [panelId]; - } else { - panelsList = Object.keys(this.panels); - } - var self = this; - panelsList.forEach(function(pid) { - self.panels[pid].data_layer_ids_by_z_index.forEach(function(dlid){ - var layer = self.panels[pid].data_layers[dlid]; - layer.destroyAllTooltips(); - - delete self.layout.state[pid + "." + dlid]; - if(mode === "reset") { - layer.setDefaultState(); + LocusZoom.Plot.prototype.clearPanelData = function (panelId, mode) { + mode = mode || 'wipe'; + // TODO: Add unit tests for this method + var panelsList; + if (panelId) { + panelsList = [panelId]; + } else { + panelsList = Object.keys(this.panels); } - }); - }); - return this; -}; - -/** + var self = this; + panelsList.forEach(function (pid) { + self.panels[pid].data_layer_ids_by_z_index.forEach(function (dlid) { + var layer = self.panels[pid].data_layers[dlid]; + layer.destroyAllTooltips(); + delete self.layout.state[pid + '.' + dlid]; + if (mode === 'reset') { + layer.setDefaultState(); + } + }); + }); + return this; + }; + /** * Remove the panel from the plot, and clear any state, tooltips, or other visual elements belonging to nested content * @param {String} id * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.removePanel = function(id){ - if (!this.panels[id]){ - throw ("Unable to remove panel, ID not found: " + id); - } - - // Hide all panel boundaries - this.panel_boundaries.hide(); - - // Destroy all tooltips and state vars for all data layers on the panel - this.clearPanelData(id); - - // Remove all panel-level HTML overlay elements - this.panels[id].loader.hide(); - this.panels[id].dashboard.destroy(true); - this.panels[id].curtain.hide(); - - // Remove the svg container for the panel if it exists - if (this.panels[id].svg.container){ - this.panels[id].svg.container.remove(); - } - - // Delete the panel and its presence in the plot layout and state - this.layout.panels.splice(this.panels[id].layout_idx, 1); - delete this.panels[id]; - delete this.layout.state[id]; - - // Update layout_idx values for all remaining panels - this.layout.panels.forEach(function(panel_layout, idx){ - this.panels[panel_layout.id].layout_idx = idx; - }.bind(this)); - - // Remove the panel id from the y_index array - this.panel_ids_by_y_index.splice(this.panel_ids_by_y_index.indexOf(id), 1); - this.applyPanelYIndexesToPanelLayouts(); - - // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space - if (this.initialized){ - // Allow the plot to shrink when panels are removed, by forcing it to recalculate min dimensions from scratch - this.layout.min_height = this._base_layout.min_height; - this.layout.min_width = this._base_layout.min_width; - - this.positionPanels(); - // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip - // positioning. TODO: make this additional call unnecessary. - this.setDimensions(this.layout.width, this.layout.height); - } - - return this; -}; - - -/** + LocusZoom.Plot.prototype.removePanel = function (id) { + if (!this.panels[id]) { + throw 'Unable to remove panel, ID not found: ' + id; + } + // Hide all panel boundaries + this.panel_boundaries.hide(); + // Destroy all tooltips and state vars for all data layers on the panel + this.clearPanelData(id); + // Remove all panel-level HTML overlay elements + this.panels[id].loader.hide(); + this.panels[id].dashboard.destroy(true); + this.panels[id].curtain.hide(); + // Remove the svg container for the panel if it exists + if (this.panels[id].svg.container) { + this.panels[id].svg.container.remove(); + } + // Delete the panel and its presence in the plot layout and state + this.layout.panels.splice(this.panels[id].layout_idx, 1); + delete this.panels[id]; + delete this.layout.state[id]; + // Update layout_idx values for all remaining panels + this.layout.panels.forEach(function (panel_layout, idx) { + this.panels[panel_layout.id].layout_idx = idx; + }.bind(this)); + // Remove the panel id from the y_index array + this.panel_ids_by_y_index.splice(this.panel_ids_by_y_index.indexOf(id), 1); + this.applyPanelYIndexesToPanelLayouts(); + // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space + if (this.initialized) { + // Allow the plot to shrink when panels are removed, by forcing it to recalculate min dimensions from scratch + this.layout.min_height = this._base_layout.min_height; + this.layout.min_width = this._base_layout.min_width; + this.positionPanels(); + // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip + // positioning. TODO: make this additional call unnecessary. + this.setDimensions(this.layout.width, this.layout.height); + } + return this; + }; + /** * Automatically position panels based on panel positioning rules and values. * Keep panels from overlapping vertically by adjusting origins, and keep the sum of proportional heights at 1. * @@ -9122,617 +9090,553 @@ LocusZoom.Plot.prototype.removePanel = function(id){ * but the logic for keeping these user-definable values straight approaches the complexity of a 2D box-packing algorithm. * That's complexity we don't need right now, and may not ever need, so it's on hiatus until a use case materializes. */ -LocusZoom.Plot.prototype.positionPanels = function(){ - - var id; - - // We want to enforce that all x-linked panels have consistent horizontal margins - // (to ensure that aligned items stay aligned despite inconsistent initial layout parameters) - // NOTE: This assumes panels have consistent widths already. That should probably be enforced too! - var x_linked_margins = { left: 0, right: 0 }; - - // Proportional heights for newly added panels default to null unless explicitly set, so determine appropriate - // proportional heights for all panels with a null value from discretely set dimensions. - // Likewise handle default nulls for proportional widths, but instead just force a value of 1 (full width) - for (id in this.panels){ - if (this.panels[id].layout.proportional_height === null){ - this.panels[id].layout.proportional_height = this.panels[id].layout.height / this.layout.height; - } - if (this.panels[id].layout.proportional_width === null){ - this.panels[id].layout.proportional_width = 1; - } - if (this.panels[id].layout.interaction.x_linked){ - x_linked_margins.left = Math.max(x_linked_margins.left, this.panels[id].layout.margin.left); - x_linked_margins.right = Math.max(x_linked_margins.right, this.panels[id].layout.margin.right); - } - } - - // Sum the proportional heights and then adjust all proportionally so that the sum is exactly 1 - var total_proportional_height = this.sumProportional("height"); - if (!total_proportional_height){ - return this; - } - var proportional_adjustment = 1 / total_proportional_height; - for (id in this.panels){ - this.panels[id].layout.proportional_height *= proportional_adjustment; - } - - // Update origins on all panels without changing plot-level dimensions yet - // Also apply x-linked margins to x-linked panels, updating widths as needed - var y_offset = 0; - this.panel_ids_by_y_index.forEach(function(panel_id){ - this.panels[panel_id].setOrigin(0, y_offset); - this.panels[panel_id].layout.proportional_origin.x = 0; - y_offset += this.panels[panel_id].layout.height; - if (this.panels[panel_id].layout.interaction.x_linked){ - var delta = Math.max(x_linked_margins.left - this.panels[panel_id].layout.margin.left, 0) - + Math.max(x_linked_margins.right - this.panels[panel_id].layout.margin.right, 0); - this.panels[panel_id].layout.width += delta; - this.panels[panel_id].layout.margin.left = x_linked_margins.left; - this.panels[panel_id].layout.margin.right = x_linked_margins.right; - this.panels[panel_id].layout.cliparea.origin.x = x_linked_margins.left; - } - }.bind(this)); - var calculated_plot_height = y_offset; - this.panel_ids_by_y_index.forEach(function(panel_id){ - this.panels[panel_id].layout.proportional_origin.y = this.panels[panel_id].layout.origin.y / calculated_plot_height; - }.bind(this)); - - // Update dimensions on the plot to accommodate repositioned panels - this.setDimensions(); - - // Set dimensions on all panels using newly set plot-level dimensions and panel-level proportional dimensions - this.panel_ids_by_y_index.forEach(function(panel_id){ - this.panels[panel_id].setDimensions(this.layout.width * this.panels[panel_id].layout.proportional_width, - this.layout.height * this.panels[panel_id].layout.proportional_height); - }.bind(this)); - - return this; - -}; - -/** + LocusZoom.Plot.prototype.positionPanels = function () { + var id; + // We want to enforce that all x-linked panels have consistent horizontal margins + // (to ensure that aligned items stay aligned despite inconsistent initial layout parameters) + // NOTE: This assumes panels have consistent widths already. That should probably be enforced too! + var x_linked_margins = { + left: 0, + right: 0 + }; + // Proportional heights for newly added panels default to null unless explicitly set, so determine appropriate + // proportional heights for all panels with a null value from discretely set dimensions. + // Likewise handle default nulls for proportional widths, but instead just force a value of 1 (full width) + for (id in this.panels) { + if (this.panels[id].layout.proportional_height === null) { + this.panels[id].layout.proportional_height = this.panels[id].layout.height / this.layout.height; + } + if (this.panels[id].layout.proportional_width === null) { + this.panels[id].layout.proportional_width = 1; + } + if (this.panels[id].layout.interaction.x_linked) { + x_linked_margins.left = Math.max(x_linked_margins.left, this.panels[id].layout.margin.left); + x_linked_margins.right = Math.max(x_linked_margins.right, this.panels[id].layout.margin.right); + } + } + // Sum the proportional heights and then adjust all proportionally so that the sum is exactly 1 + var total_proportional_height = this.sumProportional('height'); + if (!total_proportional_height) { + return this; + } + var proportional_adjustment = 1 / total_proportional_height; + for (id in this.panels) { + this.panels[id].layout.proportional_height *= proportional_adjustment; + } + // Update origins on all panels without changing plot-level dimensions yet + // Also apply x-linked margins to x-linked panels, updating widths as needed + var y_offset = 0; + this.panel_ids_by_y_index.forEach(function (panel_id) { + this.panels[panel_id].setOrigin(0, y_offset); + this.panels[panel_id].layout.proportional_origin.x = 0; + y_offset += this.panels[panel_id].layout.height; + if (this.panels[panel_id].layout.interaction.x_linked) { + var delta = Math.max(x_linked_margins.left - this.panels[panel_id].layout.margin.left, 0) + Math.max(x_linked_margins.right - this.panels[panel_id].layout.margin.right, 0); + this.panels[panel_id].layout.width += delta; + this.panels[panel_id].layout.margin.left = x_linked_margins.left; + this.panels[panel_id].layout.margin.right = x_linked_margins.right; + this.panels[panel_id].layout.cliparea.origin.x = x_linked_margins.left; + } + }.bind(this)); + var calculated_plot_height = y_offset; + this.panel_ids_by_y_index.forEach(function (panel_id) { + this.panels[panel_id].layout.proportional_origin.y = this.panels[panel_id].layout.origin.y / calculated_plot_height; + }.bind(this)); + // Update dimensions on the plot to accommodate repositioned panels + this.setDimensions(); + // Set dimensions on all panels using newly set plot-level dimensions and panel-level proportional dimensions + this.panel_ids_by_y_index.forEach(function (panel_id) { + this.panels[panel_id].setDimensions(this.layout.width * this.panels[panel_id].layout.proportional_width, this.layout.height * this.panels[panel_id].layout.proportional_height); + }.bind(this)); + return this; + }; + /** * Prepare the first rendering of the plot. This includes initializing the individual panels, but also creates shared * elements such as mouse events, panel guides/boundaries, and loader/curtain. * * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.initialize = function(){ - - // Ensure proper responsive class is present on the containing node if called for - if (this.layout.responsive_resize){ - d3.select(this.container).classed("lz-container-responsive", true); - } - - // Create an element/layer for containing mouse guides - if (this.layout.mouse_guide) { - var mouse_guide_svg = this.svg.append("g") - .attr("class", "lz-mouse_guide").attr("id", this.id + ".mouse_guide"); - var mouse_guide_vertical_svg = mouse_guide_svg.append("rect") - .attr("class", "lz-mouse_guide-vertical").attr("x",-1); - var mouse_guide_horizontal_svg = mouse_guide_svg.append("rect") - .attr("class", "lz-mouse_guide-horizontal").attr("y",-1); - this.mouse_guide = { - svg: mouse_guide_svg, - vertical: mouse_guide_vertical_svg, - horizontal: mouse_guide_horizontal_svg - }; - } - - // Add curtain and loader prototpyes to the plot - this.curtain = LocusZoom.generateCurtain.call(this); - this.loader = LocusZoom.generateLoader.call(this); - - // Create the panel_boundaries object with show/position/hide methods - this.panel_boundaries = { - parent: this, - hide_timeout: null, - showing: false, - dragging: false, - selectors: [], - corner_selector: null, - show: function(){ - // Generate panel boundaries - if (!this.showing && !this.parent.curtain.showing){ - this.showing = true; - // Loop through all panels to create a horizontal boundary for each - this.parent.panel_ids_by_y_index.forEach(function(panel_id, panel_idx){ - var selector = d3.select(this.parent.svg.node().parentNode).insert("div", ".lz-data_layer-tooltip") - .attr("class", "lz-panel-boundary") - .attr("title", "Resize panel"); - selector.append("span"); - var panel_resize_drag = d3.behavior.drag(); - panel_resize_drag.on("dragstart", function(){ this.dragging = true; }.bind(this)); - panel_resize_drag.on("dragend", function(){ this.dragging = false; }.bind(this)); - panel_resize_drag.on("drag", function(){ - // First set the dimensions on the panel we're resizing - var this_panel = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]]; - var original_panel_height = this_panel.layout.height; - this_panel.setDimensions(this_panel.layout.width, this_panel.layout.height + d3.event.dy); - var panel_height_change = this_panel.layout.height - original_panel_height; - var new_calculated_plot_height = this.parent.layout.height + panel_height_change; - // Next loop through all panels. - // Update proportional dimensions for all panels including the one we've resized using discrete heights. - // Reposition panels with a greater y-index than this panel to their appropriate new origin. - this.parent.panel_ids_by_y_index.forEach(function(loop_panel_id, loop_panel_idx){ - var loop_panel = this.parent.panels[this.parent.panel_ids_by_y_index[loop_panel_idx]]; - loop_panel.layout.proportional_height = loop_panel.layout.height / new_calculated_plot_height; - if (loop_panel_idx > panel_idx){ - loop_panel.setOrigin(loop_panel.layout.origin.x, loop_panel.layout.origin.y + panel_height_change); - loop_panel.dashboard.position(); - } + LocusZoom.Plot.prototype.initialize = function () { + // Ensure proper responsive class is present on the containing node if called for + if (this.layout.responsive_resize) { + d3.select(this.container).classed('lz-container-responsive', true); + } + // Create an element/layer for containing mouse guides + if (this.layout.mouse_guide) { + var mouse_guide_svg = this.svg.append('g').attr('class', 'lz-mouse_guide').attr('id', this.id + '.mouse_guide'); + var mouse_guide_vertical_svg = mouse_guide_svg.append('rect').attr('class', 'lz-mouse_guide-vertical').attr('x', -1); + var mouse_guide_horizontal_svg = mouse_guide_svg.append('rect').attr('class', 'lz-mouse_guide-horizontal').attr('y', -1); + this.mouse_guide = { + svg: mouse_guide_svg, + vertical: mouse_guide_vertical_svg, + horizontal: mouse_guide_horizontal_svg + }; + } + // Add curtain and loader prototpyes to the plot + this.curtain = LocusZoom.generateCurtain.call(this); + this.loader = LocusZoom.generateLoader.call(this); + // Create the panel_boundaries object with show/position/hide methods + this.panel_boundaries = { + parent: this, + hide_timeout: null, + showing: false, + dragging: false, + selectors: [], + corner_selector: null, + show: function () { + // Generate panel boundaries + if (!this.showing && !this.parent.curtain.showing) { + this.showing = true; + // Loop through all panels to create a horizontal boundary for each + this.parent.panel_ids_by_y_index.forEach(function (panel_id, panel_idx) { + var selector = d3.select(this.parent.svg.node().parentNode).insert('div', '.lz-data_layer-tooltip').attr('class', 'lz-panel-boundary').attr('title', 'Resize panel'); + selector.append('span'); + var panel_resize_drag = d3.behavior.drag(); + panel_resize_drag.on('dragstart', function () { + this.dragging = true; + }.bind(this)); + panel_resize_drag.on('dragend', function () { + this.dragging = false; + }.bind(this)); + panel_resize_drag.on('drag', function () { + // First set the dimensions on the panel we're resizing + var this_panel = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]]; + var original_panel_height = this_panel.layout.height; + this_panel.setDimensions(this_panel.layout.width, this_panel.layout.height + d3.event.dy); + var panel_height_change = this_panel.layout.height - original_panel_height; + var new_calculated_plot_height = this.parent.layout.height + panel_height_change; + // Next loop through all panels. + // Update proportional dimensions for all panels including the one we've resized using discrete heights. + // Reposition panels with a greater y-index than this panel to their appropriate new origin. + this.parent.panel_ids_by_y_index.forEach(function (loop_panel_id, loop_panel_idx) { + var loop_panel = this.parent.panels[this.parent.panel_ids_by_y_index[loop_panel_idx]]; + loop_panel.layout.proportional_height = loop_panel.layout.height / new_calculated_plot_height; + if (loop_panel_idx > panel_idx) { + loop_panel.setOrigin(loop_panel.layout.origin.x, loop_panel.layout.origin.y + panel_height_change); + loop_panel.dashboard.position(); + } + }.bind(this)); + // Reset dimensions on the entire plot and reposition panel boundaries + this.parent.positionPanels(); + this.position(); + }.bind(this)); + selector.call(panel_resize_drag); + this.parent.panel_boundaries.selectors.push(selector); + }.bind(this)); + // Create a corner boundary / resize element on the bottom-most panel that resizes the entire plot + var corner_selector = d3.select(this.parent.svg.node().parentNode).insert('div', '.lz-data_layer-tooltip').attr('class', 'lz-panel-corner-boundary').attr('title', 'Resize plot'); + corner_selector.append('span').attr('class', 'lz-panel-corner-boundary-outer'); + corner_selector.append('span').attr('class', 'lz-panel-corner-boundary-inner'); + var corner_drag = d3.behavior.drag(); + corner_drag.on('dragstart', function () { + this.dragging = true; }.bind(this)); - // Reset dimensions on the entire plot and reposition panel boundaries - this.parent.positionPanels(); - this.position(); + corner_drag.on('dragend', function () { + this.dragging = false; + }.bind(this)); + corner_drag.on('drag', function () { + this.setDimensions(this.layout.width + d3.event.dx, this.layout.height + d3.event.dy); + }.bind(this.parent)); + corner_selector.call(corner_drag); + this.parent.panel_boundaries.corner_selector = corner_selector; + } + return this.position(); + }, + position: function () { + if (!this.showing) { + return this; + } + // Position panel boundaries + var plot_page_origin = this.parent.getPageOrigin(); + this.selectors.forEach(function (selector, panel_idx) { + var panel_page_origin = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].getPageOrigin(); + var left = plot_page_origin.x; + var top = panel_page_origin.y + this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].layout.height - 12; + var width = this.parent.layout.width - 1; + selector.style({ + top: top + 'px', + left: left + 'px', + width: width + 'px' + }); + selector.select('span').style({ width: width + 'px' }); }.bind(this)); - selector.call(panel_resize_drag); - this.parent.panel_boundaries.selectors.push(selector); + // Position corner selector + var corner_padding = 10; + var corner_size = 16; + this.corner_selector.style({ + top: plot_page_origin.y + this.parent.layout.height - corner_padding - corner_size + 'px', + left: plot_page_origin.x + this.parent.layout.width - corner_padding - corner_size + 'px' + }); + return this; + }, + hide: function () { + if (!this.showing) { + return this; + } + this.showing = false; + // Remove panel boundaries + this.selectors.forEach(function (selector) { + selector.remove(); + }); + this.selectors = []; + // Remove corner boundary + this.corner_selector.remove(); + this.corner_selector = null; + return this; + } + }; + // Show panel boundaries stipulated by the layout (basic toggle, only show on mouse over plot) + if (this.layout.panel_boundaries) { + d3.select(this.svg.node().parentNode).on('mouseover.' + this.id + '.panel_boundaries', function () { + clearTimeout(this.panel_boundaries.hide_timeout); + this.panel_boundaries.show(); + }.bind(this)); + d3.select(this.svg.node().parentNode).on('mouseout.' + this.id + '.panel_boundaries', function () { + this.panel_boundaries.hide_timeout = setTimeout(function () { + this.panel_boundaries.hide(); + }.bind(this), 300); }.bind(this)); - // Create a corner boundary / resize element on the bottom-most panel that resizes the entire plot - var corner_selector = d3.select(this.parent.svg.node().parentNode).insert("div", ".lz-data_layer-tooltip") - .attr("class", "lz-panel-corner-boundary") - .attr("title", "Resize plot"); - corner_selector.append("span").attr("class", "lz-panel-corner-boundary-outer"); - corner_selector.append("span").attr("class", "lz-panel-corner-boundary-inner"); - var corner_drag = d3.behavior.drag(); - corner_drag.on("dragstart", function(){ this.dragging = true; }.bind(this)); - corner_drag.on("dragend", function(){ this.dragging = false; }.bind(this)); - corner_drag.on("drag", function(){ - this.setDimensions(this.layout.width + d3.event.dx, this.layout.height + d3.event.dy); - }.bind(this.parent)); - corner_selector.call(corner_drag); - this.parent.panel_boundaries.corner_selector = corner_selector; } - return this.position(); - }, - position: function(){ - if (!this.showing){ return this; } - // Position panel boundaries - var plot_page_origin = this.parent.getPageOrigin(); - this.selectors.forEach(function(selector, panel_idx){ - var panel_page_origin = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].getPageOrigin(); - var left = plot_page_origin.x; - var top = panel_page_origin.y + this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].layout.height - 12; - var width = this.parent.layout.width - 1; - selector.style({ - top: top + "px", - left: left + "px", - width: width + "px" - }); - selector.select("span").style({ - width: width + "px" - }); - }.bind(this)); - // Position corner selector - var corner_padding = 10; - var corner_size = 16; - this.corner_selector.style({ - top: (plot_page_origin.y + this.parent.layout.height - corner_padding - corner_size) + "px", - left: (plot_page_origin.x + this.parent.layout.width - corner_padding - corner_size) + "px" - }); - return this; - }, - hide: function(){ - if (!this.showing){ return this; } - this.showing = false; - // Remove panel boundaries - this.selectors.forEach(function(selector){ selector.remove(); }); - this.selectors = []; - // Remove corner boundary - this.corner_selector.remove(); - this.corner_selector = null; + // Create the dashboard object and immediately show it + this.dashboard = new LocusZoom.Dashboard(this).show(); + // Initialize all panels + for (var id in this.panels) { + this.panels[id].initialize(); + } + // Define plot-level mouse events + var namespace = '.' + this.id; + if (this.layout.mouse_guide) { + var mouseout_mouse_guide = function () { + this.mouse_guide.vertical.attr('x', -1); + this.mouse_guide.horizontal.attr('y', -1); + }.bind(this); + var mousemove_mouse_guide = function () { + var coords = d3.mouse(this.svg.node()); + this.mouse_guide.vertical.attr('x', coords[0]); + this.mouse_guide.horizontal.attr('y', coords[1]); + }.bind(this); + this.svg.on('mouseout' + namespace + '-mouse_guide', mouseout_mouse_guide).on('touchleave' + namespace + '-mouse_guide', mouseout_mouse_guide).on('mousemove' + namespace + '-mouse_guide', mousemove_mouse_guide); + } + var mouseup = function () { + this.stopDrag(); + }.bind(this); + var mousemove = function () { + if (this.interaction.dragging) { + var coords = d3.mouse(this.svg.node()); + if (d3.event) { + d3.event.preventDefault(); + } + this.interaction.dragging.dragged_x = coords[0] - this.interaction.dragging.start_x; + this.interaction.dragging.dragged_y = coords[1] - this.interaction.dragging.start_y; + this.panels[this.interaction.panel_id].render(); + this.interaction.linked_panel_ids.forEach(function (panel_id) { + this.panels[panel_id].render(); + }.bind(this)); + } + }.bind(this); + this.svg.on('mouseup' + namespace, mouseup).on('touchend' + namespace, mouseup).on('mousemove' + namespace, mousemove).on('touchmove' + namespace, mousemove); + // Add an extra namespaced mouseup handler to the containing body, if there is one + // This helps to stop interaction events gracefully when dragging outside of the plot element + if (!d3.select('body').empty()) { + d3.select('body').on('mouseup' + namespace, mouseup).on('touchend' + namespace, mouseup); + } + this.initialized = true; + // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip + // positioning. TODO: make this additional call unnecessary. + var client_rect = this.svg.node().getBoundingClientRect(); + var width = client_rect.width ? client_rect.width : this.layout.width; + var height = client_rect.height ? client_rect.height : this.layout.height; + this.setDimensions(width, height); return this; - } - }; - - // Show panel boundaries stipulated by the layout (basic toggle, only show on mouse over plot) - if (this.layout.panel_boundaries){ - d3.select(this.svg.node().parentNode).on("mouseover." + this.id + ".panel_boundaries", function(){ - clearTimeout(this.panel_boundaries.hide_timeout); - this.panel_boundaries.show(); - }.bind(this)); - d3.select(this.svg.node().parentNode).on("mouseout." + this.id + ".panel_boundaries", function(){ - this.panel_boundaries.hide_timeout = setTimeout(function(){ - this.panel_boundaries.hide(); - }.bind(this), 300); - }.bind(this)); - } - - // Create the dashboard object and immediately show it - this.dashboard = new LocusZoom.Dashboard(this).show(); - - // Initialize all panels - for (var id in this.panels){ - this.panels[id].initialize(); - } - - // Define plot-level mouse events - var namespace = "." + this.id; - if (this.layout.mouse_guide) { - var mouseout_mouse_guide = function(){ - this.mouse_guide.vertical.attr("x", -1); - this.mouse_guide.horizontal.attr("y", -1); - }.bind(this); - var mousemove_mouse_guide = function(){ - var coords = d3.mouse(this.svg.node()); - this.mouse_guide.vertical.attr("x", coords[0]); - this.mouse_guide.horizontal.attr("y", coords[1]); - }.bind(this); - this.svg - .on("mouseout" + namespace + "-mouse_guide", mouseout_mouse_guide) - .on("touchleave" + namespace + "-mouse_guide", mouseout_mouse_guide) - .on("mousemove" + namespace + "-mouse_guide", mousemove_mouse_guide); - } - var mouseup = function(){ - this.stopDrag(); - }.bind(this); - var mousemove = function(){ - if (this.interaction.dragging){ - var coords = d3.mouse(this.svg.node()); - if (d3.event){ d3.event.preventDefault(); } - this.interaction.dragging.dragged_x = coords[0] - this.interaction.dragging.start_x; - this.interaction.dragging.dragged_y = coords[1] - this.interaction.dragging.start_y; - this.panels[this.interaction.panel_id].render(); - this.interaction.linked_panel_ids.forEach(function(panel_id){ - this.panels[panel_id].render(); - }.bind(this)); - } - }.bind(this); - this.svg - .on("mouseup" + namespace, mouseup) - .on("touchend" + namespace, mouseup) - .on("mousemove" + namespace, mousemove) - .on("touchmove" + namespace, mousemove); - - // Add an extra namespaced mouseup handler to the containing body, if there is one - // This helps to stop interaction events gracefully when dragging outside of the plot element - if (!d3.select("body").empty()){ - d3.select("body") - .on("mouseup" + namespace, mouseup) - .on("touchend" + namespace, mouseup); - } - - this.initialized = true; - - // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip - // positioning. TODO: make this additional call unnecessary. - var client_rect = this.svg.node().getBoundingClientRect(); - var width = client_rect.width ? client_rect.width : this.layout.width; - var height = client_rect.height ? client_rect.height : this.layout.height; - this.setDimensions(width, height); - - return this; - -}; - -/** + }; + /** * Refresh (or fetch) a plot's data from sources, regardless of whether position or state has changed * @returns {Promise} */ -LocusZoom.Plot.prototype.refresh = function(){ - return this.applyState(); -}; - -/** + LocusZoom.Plot.prototype.refresh = function () { + return this.applyState(); + }; + /** * Update state values and trigger a pull for fresh data on all data sources for all data layers * @param state_changes * @returns {Promise} A promise that resolves when all data fetch and update operations are complete */ -LocusZoom.Plot.prototype.applyState = function(state_changes){ - - state_changes = state_changes || {}; - if (typeof state_changes != "object"){ - throw("LocusZoom.applyState only accepts an object; " + (typeof state_changes) + " given"); - } - - // First make a copy of the current (old) state to work with - var new_state = JSON.parse(JSON.stringify(this.state)); - - // Apply changes by top-level property to the new state - for (var property in state_changes) { - new_state[property] = state_changes[property]; - } - - // Validate the new state (may do nothing, may do a lot, depends on how the user has things set up) - new_state = LocusZoom.validateState(new_state, this.layout); - - // Apply new state to the actual state - for (property in new_state) { - this.state[property] = new_state[property]; - } - - // Generate requests for all panels given new state - this.emit("data_requested"); - this.remap_promises = []; - this.loading_data = true; - for (var id in this.panels){ - this.remap_promises.push(this.panels[id].reMap()); - } - - return Q.all(this.remap_promises) - .catch(function(error){ - console.error(error); - this.curtain.drop(error); - this.loading_data = false; - }.bind(this)) - .then(function(){ - // TODO: Check logic here; in some promise implementations, this would cause the error to be considered handled, and "then" would always fire. (may or may not be desired behavior) - // Update dashboard / components - this.dashboard.update(); - - // Apply panel-level state values - this.panel_ids_by_y_index.forEach(function(panel_id){ - var panel = this.panels[panel_id]; - panel.dashboard.update(); - // Apply data-layer-level state values - panel.data_layer_ids_by_z_index.forEach(function(data_layer_id){ - var data_layer = this.data_layers[data_layer_id]; - var state_id = panel_id + "." + data_layer_id; - for (var property in this.state[state_id]){ - if (!this.state[state_id].hasOwnProperty(property)){ continue; } - if (Array.isArray(this.state[state_id][property])){ - this.state[state_id][property].forEach(function(element_id){ - try { - this.setElementStatus(property, this.getElementById(element_id), true); - } catch (e){ - console.error("Unable to apply state: " + state_id + ", " + property); - } - }.bind(data_layer)); + LocusZoom.Plot.prototype.applyState = function (state_changes) { + state_changes = state_changes || {}; + if (typeof state_changes != 'object') { + throw 'LocusZoom.applyState only accepts an object; ' + typeof state_changes + ' given'; + } + // First make a copy of the current (old) state to work with + var new_state = JSON.parse(JSON.stringify(this.state)); + // Apply changes by top-level property to the new state + for (var property in state_changes) { + new_state[property] = state_changes[property]; + } + // Validate the new state (may do nothing, may do a lot, depends on how the user has things set up) + new_state = LocusZoom.validateState(new_state, this.layout); + // Apply new state to the actual state + for (property in new_state) { + this.state[property] = new_state[property]; + } + // Generate requests for all panels given new state + this.emit('data_requested'); + this.remap_promises = []; + this.loading_data = true; + for (var id in this.panels) { + this.remap_promises.push(this.panels[id].reMap()); + } + return Q.all(this.remap_promises).catch(function (error) { + console.error(error); + this.curtain.drop(error); + this.loading_data = false; + }.bind(this)).then(function () { + // TODO: Check logic here; in some promise implementations, this would cause the error to be considered handled, and "then" would always fire. (may or may not be desired behavior) + // Update dashboard / components + this.dashboard.update(); + // Apply panel-level state values + this.panel_ids_by_y_index.forEach(function (panel_id) { + var panel = this.panels[panel_id]; + panel.dashboard.update(); + // Apply data-layer-level state values + panel.data_layer_ids_by_z_index.forEach(function (data_layer_id) { + var data_layer = this.data_layers[data_layer_id]; + var state_id = panel_id + '.' + data_layer_id; + for (var property in this.state[state_id]) { + if (!this.state[state_id].hasOwnProperty(property)) { + continue; + } + if (Array.isArray(this.state[state_id][property])) { + this.state[state_id][property].forEach(function (element_id) { + try { + this.setElementStatus(property, this.getElementById(element_id), true); + } catch (e) { + console.error('Unable to apply state: ' + state_id + ', ' + property); + } + }.bind(data_layer)); + } } - } - }.bind(panel)); + }.bind(panel)); + }.bind(this)); + // Emit events + this.emit('layout_changed'); + this.emit('data_rendered'); + this.loading_data = false; }.bind(this)); - - // Emit events - this.emit("layout_changed"); - this.emit("data_rendered"); - - this.loading_data = false; - - }.bind(this)); -}; - -/** + }; + /** * Register interactions along the specified axis, provided that the target panel allows interaction. * * @param {LocusZoom.Panel} panel * @param {('x_tick'|'y1_tick'|'y2_tick')} method The direction (axis) along which dragging is being performed. * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.startDrag = function(panel, method){ - - panel = panel || null; - method = method || null; - - var axis = null; - switch (method){ - case "background": - case "x_tick": - axis = "x"; - break; - case "y1_tick": - axis = "y1"; - break; - case "y2_tick": - axis = "y2"; - break; - } - - if (!(panel instanceof LocusZoom.Panel) || !axis || !this.canInteract()){ return this.stopDrag(); } - - var coords = d3.mouse(this.svg.node()); - this.interaction = { - panel_id: panel.id, - linked_panel_ids: panel.getLinkedPanelIds(axis), - dragging: { - method: method, - start_x: coords[0], - start_y: coords[1], - dragged_x: 0, - dragged_y: 0, - axis: axis - } - }; - - this.svg.style("cursor", "all-scroll"); - - return this; - -}; - -/** + LocusZoom.Plot.prototype.startDrag = function (panel, method) { + panel = panel || null; + method = method || null; + var axis = null; + switch (method) { + case 'background': + case 'x_tick': + axis = 'x'; + break; + case 'y1_tick': + axis = 'y1'; + break; + case 'y2_tick': + axis = 'y2'; + break; + } + if (!(panel instanceof LocusZoom.Panel) || !axis || !this.canInteract()) { + return this.stopDrag(); + } + var coords = d3.mouse(this.svg.node()); + this.interaction = { + panel_id: panel.id, + linked_panel_ids: panel.getLinkedPanelIds(axis), + dragging: { + method: method, + start_x: coords[0], + start_y: coords[1], + dragged_x: 0, + dragged_y: 0, + axis: axis + } + }; + this.svg.style('cursor', 'all-scroll'); + return this; + }; + /** * Process drag interactions across the target panel and synchronize plot state across other panels in sync; * clear the event when complete * @returns {LocusZoom.Plot} */ -LocusZoom.Plot.prototype.stopDrag = function(){ - - if (!this.interaction.dragging){ return this; } - - if (typeof this.panels[this.interaction.panel_id] != "object"){ - this.interaction = {}; - return this; - } - var panel = this.panels[this.interaction.panel_id]; - - // Helper function to find the appropriate axis layouts on child data layers - // Once found, apply the extent as floor/ceiling and remove all other directives - // This forces all associated axes to conform to the extent generated by a drag action - var overrideAxisLayout = function(axis, axis_number, extent){ - panel.data_layer_ids_by_z_index.forEach(function(id){ - if (panel.data_layers[id].layout[axis+"_axis"].axis === axis_number){ - panel.data_layers[id].layout[axis+"_axis"].floor = extent[0]; - panel.data_layers[id].layout[axis+"_axis"].ceiling = extent[1]; - delete panel.data_layers[id].layout[axis+"_axis"].lower_buffer; - delete panel.data_layers[id].layout[axis+"_axis"].upper_buffer; - delete panel.data_layers[id].layout[axis+"_axis"].min_extent; - delete panel.data_layers[id].layout[axis+"_axis"].ticks; + LocusZoom.Plot.prototype.stopDrag = function () { + if (!this.interaction.dragging) { + return this; } - }); - }; - - switch(this.interaction.dragging.method){ - case "background": - case "x_tick": - if (this.interaction.dragging.dragged_x !== 0){ - overrideAxisLayout("x", 1, panel.x_extent); - this.applyState({ start: panel.x_extent[0], end: panel.x_extent[1] }); - } - break; - case "y1_tick": - case "y2_tick": - if (this.interaction.dragging.dragged_y !== 0){ - // TODO: Hardcoded assumption of only two possible axes with single-digit #s (switch/case) - var y_axis_number = parseInt(this.interaction.dragging.method[1]); - overrideAxisLayout("y", y_axis_number, panel["y"+y_axis_number+"_extent"]); - } - break; - } - - this.interaction = {}; - this.svg.style("cursor", null); - - return this; - -}; - -/* global LocusZoom */ -"use strict"; - -/** + if (typeof this.panels[this.interaction.panel_id] != 'object') { + this.interaction = {}; + return this; + } + var panel = this.panels[this.interaction.panel_id]; + // Helper function to find the appropriate axis layouts on child data layers + // Once found, apply the extent as floor/ceiling and remove all other directives + // This forces all associated axes to conform to the extent generated by a drag action + var overrideAxisLayout = function (axis, axis_number, extent) { + panel.data_layer_ids_by_z_index.forEach(function (id) { + if (panel.data_layers[id].layout[axis + '_axis'].axis === axis_number) { + panel.data_layers[id].layout[axis + '_axis'].floor = extent[0]; + panel.data_layers[id].layout[axis + '_axis'].ceiling = extent[1]; + delete panel.data_layers[id].layout[axis + '_axis'].lower_buffer; + delete panel.data_layers[id].layout[axis + '_axis'].upper_buffer; + delete panel.data_layers[id].layout[axis + '_axis'].min_extent; + delete panel.data_layers[id].layout[axis + '_axis'].ticks; + } + }); + }; + switch (this.interaction.dragging.method) { + case 'background': + case 'x_tick': + if (this.interaction.dragging.dragged_x !== 0) { + overrideAxisLayout('x', 1, panel.x_extent); + this.applyState({ + start: panel.x_extent[0], + end: panel.x_extent[1] + }); + } + break; + case 'y1_tick': + case 'y2_tick': + if (this.interaction.dragging.dragged_y !== 0) { + // TODO: Hardcoded assumption of only two possible axes with single-digit #s (switch/case) + var y_axis_number = parseInt(this.interaction.dragging.method[1]); + overrideAxisLayout('y', y_axis_number, panel['y' + y_axis_number + '_extent']); + } + break; + } + this.interaction = {}; + this.svg.style('cursor', null); + return this; + }; + /* global LocusZoom */ + 'use strict'; + /** * A panel is an abstract class representing a subdivision of the LocusZoom stage * to display a distinct data representation as a collection of data layers. * @class * @param {Object} layout * @param {LocusZoom.Plot|null} parent */ -LocusZoom.Panel = function(layout, parent) { - - if (typeof layout !== "object"){ - throw "Unable to create panel, invalid layout"; - } - - /** @member {LocusZoom.Plot|null} */ - this.parent = parent || null; - /** @member {LocusZoom.Plot|null} */ - this.parent_plot = parent; - - // Ensure a valid ID is present. If there is no valid ID then generate one - if (typeof layout.id !== "string" || !layout.id.length){ - if (!this.parent){ - layout.id = "p" + Math.floor(Math.random()*Math.pow(10,8)); - } else { - var id = null; - var generateID = function(){ - id = "p" + Math.floor(Math.random()*Math.pow(10,8)); - if (id == null || typeof this.parent.panels[id] != "undefined"){ - id = generateID(); + LocusZoom.Panel = function (layout, parent) { + if (typeof layout !== 'object') { + throw 'Unable to create panel, invalid layout'; + } + /** @member {LocusZoom.Plot|null} */ + this.parent = parent || null; + /** @member {LocusZoom.Plot|null} */ + this.parent_plot = parent; + // Ensure a valid ID is present. If there is no valid ID then generate one + if (typeof layout.id !== 'string' || !layout.id.length) { + if (!this.parent) { + layout.id = 'p' + Math.floor(Math.random() * Math.pow(10, 8)); + } else { + var id = null; + var generateID = function () { + id = 'p' + Math.floor(Math.random() * Math.pow(10, 8)); + if (id == null || typeof this.parent.panels[id] != 'undefined') { + id = generateID(); + } + }.bind(this); + layout.id = id; } - }.bind(this); - layout.id = id; - } - } else if (this.parent) { - if (typeof this.parent.panels[layout.id] !== "undefined"){ - throw "Cannot create panel with id [" + layout.id + "]; panel with that id already exists"; - } - } - /** @member {String} */ - this.id = layout.id; - - /** @member {Boolean} */ - this.initialized = false; - /** + } else if (this.parent) { + if (typeof this.parent.panels[layout.id] !== 'undefined') { + throw 'Cannot create panel with id [' + layout.id + ']; panel with that id already exists'; + } + } + /** @member {String} */ + this.id = layout.id; + /** @member {Boolean} */ + this.initialized = false; + /** * The index of this panel in the parent plot's `layout.panels` * @member {number} * */ - this.layout_idx = null; - /** @member {Object} */ - this.svg = {}; - - /** + this.layout_idx = null; + /** @member {Object} */ + this.svg = {}; + /** * A JSON-serializable object used to describe the composition of the Panel * @member {Object} */ - this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.Panel.DefaultLayout); - - // Define state parameters specific to this panel - if (this.parent){ - /** @member {Object} */ - this.state = this.parent.state; - - /** @member {String} */ - this.state_id = this.id; - this.state[this.state_id] = this.state[this.state_id] || {}; - } else { - this.state = null; - this.state_id = null; - } - - /** @member {Object} */ - this.data_layers = {}; - /** @member {String[]} */ - this.data_layer_ids_by_z_index = []; - - /** @protected */ - this.applyDataLayerZIndexesToDataLayerLayouts = function(){ - this.data_layer_ids_by_z_index.forEach(function(dlid, idx){ - this.data_layers[dlid].layout.z_index = idx; - }.bind(this)); - }.bind(this); - - /** + this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.Panel.DefaultLayout); + // Define state parameters specific to this panel + if (this.parent) { + /** @member {Object} */ + this.state = this.parent.state; + /** @member {String} */ + this.state_id = this.id; + this.state[this.state_id] = this.state[this.state_id] || {}; + } else { + this.state = null; + this.state_id = null; + } + /** @member {Object} */ + this.data_layers = {}; + /** @member {String[]} */ + this.data_layer_ids_by_z_index = []; + /** @protected */ + this.applyDataLayerZIndexesToDataLayerLayouts = function () { + this.data_layer_ids_by_z_index.forEach(function (dlid, idx) { + this.data_layers[dlid].layout.z_index = idx; + }.bind(this)); + }.bind(this); + /** * Track data requests in progress * @member {Promise[]} * @protected */ - this.data_promises = []; - - /** @member {d3.scale} */ - this.x_scale = null; - /** @member {d3.scale} */ - this.y1_scale = null; - /** @member {d3.scale} */ - this.y2_scale = null; - - /** @member {d3.extent} */ - this.x_extent = null; - /** @member {d3.extent} */ - this.y1_extent = null; - /** @member {d3.extent} */ - this.y2_extent = null; - - /** @member {Number[]} */ - this.x_ticks = []; - /** @member {Number[]} */ - this.y1_ticks = []; - /** @member {Number[]} */ - this.y2_ticks = []; - - /** + this.data_promises = []; + /** @member {d3.scale} */ + this.x_scale = null; + /** @member {d3.scale} */ + this.y1_scale = null; + /** @member {d3.scale} */ + this.y2_scale = null; + /** @member {d3.extent} */ + this.x_extent = null; + /** @member {d3.extent} */ + this.y1_extent = null; + /** @member {d3.extent} */ + this.y2_extent = null; + /** @member {Number[]} */ + this.x_ticks = []; + /** @member {Number[]} */ + this.y1_ticks = []; + /** @member {Number[]} */ + this.y2_ticks = []; + /** * A timeout ID as returned by setTimeout * @protected * @member {number} */ - this.zoom_timeout = null; - - /** @returns {string} */ - this.getBaseId = function(){ - return this.parent.id + "." + this.id; - }; - - /** + this.zoom_timeout = null; + /** @returns {string} */ + this.getBaseId = function () { + return this.parent.id + '.' + this.id; + }; + /** * Known event hooks that the panel can respond to * @protected * @member {Object} */ - this.event_hooks = { - "layout_changed": [], - "data_requested": [], - "data_rendered": [], - "element_clicked": [] - }; - /** + this.event_hooks = { + 'layout_changed': [], + 'data_requested': [], + 'data_rendered': [], + 'element_clicked': [] + }; + /** * There are several events that a LocusZoom panel can "emit" when appropriate, and LocusZoom supports registering * "hooks" for these events which are essentially custom functions intended to fire at certain times. * @@ -9754,156 +9658,172 @@ LocusZoom.Panel = function(layout, parent) { * @param {function} hook * @returns {LocusZoom.Panel} */ - this.on = function(event, hook){ - if (typeof "event" != "string" || !Array.isArray(this.event_hooks[event])){ - throw("Unable to register event hook, invalid event: " + event.toString()); - } - if (typeof hook != "function"){ - throw("Unable to register event hook, invalid hook function passed"); - } - this.event_hooks[event].push(hook); - return this; - }; - /** + this.on = function (event, hook) { + if (typeof 'event' != 'string' || !Array.isArray(this.event_hooks[event])) { + throw 'Unable to register event hook, invalid event: ' + event.toString(); + } + if (typeof hook != 'function') { + throw 'Unable to register event hook, invalid hook function passed'; + } + this.event_hooks[event].push(hook); + return this; + }; + /** * Handle running of event hooks when an event is emitted * @protected * @param {string} event A known event name * @param {*} context Controls function execution context (value of `this` for the hook to be fired) * @returns {LocusZoom.Panel} */ - this.emit = function(event, context){ - if (typeof "event" != "string" || !Array.isArray(this.event_hooks[event])){ - throw("LocusZoom attempted to throw an invalid event: " + event.toString()); - } - context = context || this; - this.event_hooks[event].forEach(function(hookToRun) { - hookToRun.call(context); - }); - return this; - }; - - /** + this.emit = function (event, context) { + if (typeof 'event' != 'string' || !Array.isArray(this.event_hooks[event])) { + throw 'LocusZoom attempted to throw an invalid event: ' + event.toString(); + } + context = context || this; + this.event_hooks[event].forEach(function (hookToRun) { + hookToRun.call(context); + }); + return this; + }; + /** * Get an object with the x and y coordinates of the panel's origin in terms of the entire page * Necessary for positioning any HTML elements over the panel * @returns {{x: Number, y: Number}} */ - this.getPageOrigin = function(){ - var plot_origin = this.parent.getPageOrigin(); - return { - x: plot_origin.x + this.layout.origin.x, - y: plot_origin.y + this.layout.origin.y + this.getPageOrigin = function () { + var plot_origin = this.parent.getPageOrigin(); + return { + x: plot_origin.x + this.layout.origin.x, + y: plot_origin.y + this.layout.origin.y + }; + }; + // Initialize the layout + this.initializeLayout(); + return this; }; - }; - - // Initialize the layout - this.initializeLayout(); - - return this; - -}; - -/** + /** * Default panel layout * @static * @type {Object} */ -LocusZoom.Panel.DefaultLayout = { - title: { text: "", style: {}, x: 10, y: 22 }, - y_index: null, - width: 0, - height: 0, - origin: { x: 0, y: null }, - min_width: 1, - min_height: 1, - proportional_width: null, - proportional_height: null, - proportional_origin: { x: 0, y: null }, - margin: { top: 0, right: 0, bottom: 0, left: 0 }, - background_click: "clear_selections", - dashboard: { - components: [] - }, - cliparea: { - height: 0, - width: 0, - origin: { x: 0, y: 0 } - }, - axes: { // These are the only axes supported!! - x: {}, - y1: {}, - y2: {} - }, - legend: null, - interaction: { - drag_background_to_pan: false, - drag_x_ticks_to_scale: false, - drag_y1_ticks_to_scale: false, - drag_y2_ticks_to_scale: false, - scroll_to_zoom: false, - x_linked: false, - y1_linked: false, - y2_linked: false - }, - data_layers: [] -}; - -/** + LocusZoom.Panel.DefaultLayout = { + title: { + text: '', + style: {}, + x: 10, + y: 22 + }, + y_index: null, + width: 0, + height: 0, + origin: { + x: 0, + y: null + }, + min_width: 1, + min_height: 1, + proportional_width: null, + proportional_height: null, + proportional_origin: { + x: 0, + y: null + }, + margin: { + top: 0, + right: 0, + bottom: 0, + left: 0 + }, + background_click: 'clear_selections', + dashboard: { components: [] }, + cliparea: { + height: 0, + width: 0, + origin: { + x: 0, + y: 0 + } + }, + axes: { + // These are the only axes supported!! + x: {}, + y1: {}, + y2: {} + }, + legend: null, + interaction: { + drag_background_to_pan: false, + drag_x_ticks_to_scale: false, + drag_y1_ticks_to_scale: false, + drag_y2_ticks_to_scale: false, + scroll_to_zoom: false, + x_linked: false, + y1_linked: false, + y2_linked: false + }, + data_layers: [] + }; + /** * Prepare the panel for first use by performing parameter validation, creating axes, setting default dimensions, * and preparing / positioning data layers as appropriate. * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.initializeLayout = function(){ - - // If the layout is missing BOTH width and proportional width then set the proportional width to 1. - // This will default the panel to taking up the full width of the plot. - if (this.layout.width === 0 && this.layout.proportional_width === null){ - this.layout.proportional_width = 1; - } - - // If the layout is missing BOTH height and proportional height then set the proportional height to - // an equal share of the plot's current height. - if (this.layout.height === 0 && this.layout.proportional_height === null){ - var panel_count = Object.keys(this.parent.panels).length; - if (panel_count > 0){ - this.layout.proportional_height = (1 / panel_count); - } else { - this.layout.proportional_height = 1; - } - } - - // Set panel dimensions, origin, and margin - this.setDimensions(); - this.setOrigin(); - this.setMargin(); - - // Set ranges - // TODO: Define stub values in constructor - this.x_range = [0, this.layout.cliparea.width]; - this.y1_range = [this.layout.cliparea.height, 0]; - this.y2_range = [this.layout.cliparea.height, 0]; - - // Initialize panel axes - ["x", "y1", "y2"].forEach(function(axis){ - if (!Object.keys(this.layout.axes[axis]).length || this.layout.axes[axis].render ===false){ - // The default layout sets the axis to an empty object, so set its render boolean here - this.layout.axes[axis].render = false; - } else { - this.layout.axes[axis].render = true; - this.layout.axes[axis].label = this.layout.axes[axis].label || null; - this.layout.axes[axis].label_function = this.layout.axes[axis].label_function || null; - } - }.bind(this)); - - // Add data layers (which define x and y extents) - this.layout.data_layers.forEach(function(data_layer_layout){ - this.addDataLayer(data_layer_layout); - }.bind(this)); - - return this; - -}; - -/** + LocusZoom.Panel.prototype.initializeLayout = function () { + // If the layout is missing BOTH width and proportional width then set the proportional width to 1. + // This will default the panel to taking up the full width of the plot. + if (this.layout.width === 0 && this.layout.proportional_width === null) { + this.layout.proportional_width = 1; + } + // If the layout is missing BOTH height and proportional height then set the proportional height to + // an equal share of the plot's current height. + if (this.layout.height === 0 && this.layout.proportional_height === null) { + var panel_count = Object.keys(this.parent.panels).length; + if (panel_count > 0) { + this.layout.proportional_height = 1 / panel_count; + } else { + this.layout.proportional_height = 1; + } + } + // Set panel dimensions, origin, and margin + this.setDimensions(); + this.setOrigin(); + this.setMargin(); + // Set ranges + // TODO: Define stub values in constructor + this.x_range = [ + 0, + this.layout.cliparea.width + ]; + this.y1_range = [ + this.layout.cliparea.height, + 0 + ]; + this.y2_range = [ + this.layout.cliparea.height, + 0 + ]; + // Initialize panel axes + [ + 'x', + 'y1', + 'y2' + ].forEach(function (axis) { + if (!Object.keys(this.layout.axes[axis]).length || this.layout.axes[axis].render === false) { + // The default layout sets the axis to an empty object, so set its render boolean here + this.layout.axes[axis].render = false; + } else { + this.layout.axes[axis].render = true; + this.layout.axes[axis].label = this.layout.axes[axis].label || null; + this.layout.axes[axis].label_function = this.layout.axes[axis].label_function || null; + } + }.bind(this)); + // Add data layers (which define x and y extents) + this.layout.data_layers.forEach(function (data_layer_layout) { + this.addDataLayer(data_layer_layout); + }.bind(this)); + return this; + }; + /** * Set the dimensions for the panel. If passed with no arguments will calculate optimal size based on layout * directives and the available area within the plot. If passed discrete width (number) and height (number) will * attempt to resize the panel to them, but may be limited by minimum dimensions defined on the plot or panel. @@ -9913,36 +9833,37 @@ LocusZoom.Panel.prototype.initializeLayout = function(){ * @param {number} [height] * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.setDimensions = function(width, height){ - if (typeof width != "undefined" && typeof height != "undefined"){ - if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0){ - this.layout.width = Math.max(Math.round(+width), this.layout.min_width); - this.layout.height = Math.max(Math.round(+height), this.layout.min_height); - } - } else { - if (this.layout.proportional_width !== null){ - this.layout.width = Math.max(this.layout.proportional_width * this.parent.layout.width, this.layout.min_width); - } - if (this.layout.proportional_height !== null){ - this.layout.height = Math.max(this.layout.proportional_height * this.parent.layout.height, this.layout.min_height); - } - } - this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0); - this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0); - if (this.svg.clipRect){ - this.svg.clipRect.attr("width", this.layout.width).attr("height", this.layout.height); - } - if (this.initialized){ - this.render(); - this.curtain.update(); - this.loader.update(); - this.dashboard.update(); - if (this.legend){ this.legend.position(); } - } - return this; -}; - -/** + LocusZoom.Panel.prototype.setDimensions = function (width, height) { + if (typeof width != 'undefined' && typeof height != 'undefined') { + if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0) { + this.layout.width = Math.max(Math.round(+width), this.layout.min_width); + this.layout.height = Math.max(Math.round(+height), this.layout.min_height); + } + } else { + if (this.layout.proportional_width !== null) { + this.layout.width = Math.max(this.layout.proportional_width * this.parent.layout.width, this.layout.min_width); + } + if (this.layout.proportional_height !== null) { + this.layout.height = Math.max(this.layout.proportional_height * this.parent.layout.height, this.layout.min_height); + } + } + this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0); + this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0); + if (this.svg.clipRect) { + this.svg.clipRect.attr('width', this.layout.width).attr('height', this.layout.height); + } + if (this.initialized) { + this.render(); + this.curtain.update(); + this.loader.update(); + this.dashboard.update(); + if (this.legend) { + this.legend.position(); + } + } + return this; + }; + /** * Set panel origin on the plot, and re-render as appropriate * * @public @@ -9950,14 +9871,19 @@ LocusZoom.Panel.prototype.setDimensions = function(width, height){ * @param {number} y * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.setOrigin = function(x, y){ - if (!isNaN(x) && x >= 0){ this.layout.origin.x = Math.max(Math.round(+x), 0); } - if (!isNaN(y) && y >= 0){ this.layout.origin.y = Math.max(Math.round(+y), 0); } - if (this.initialized){ this.render(); } - return this; -}; - -/** + LocusZoom.Panel.prototype.setOrigin = function (x, y) { + if (!isNaN(x) && x >= 0) { + this.layout.origin.x = Math.max(Math.round(+x), 0); + } + if (!isNaN(y) && y >= 0) { + this.layout.origin.y = Math.max(Math.round(+y), 0); + } + if (this.initialized) { + this.render(); + } + return this; + }; + /** * Set margins around this panel * @public * @param {number} top @@ -9966,35 +9892,48 @@ LocusZoom.Panel.prototype.setOrigin = function(x, y){ * @param {number} left * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.setMargin = function(top, right, bottom, left){ - var extra; - if (!isNaN(top) && top >= 0){ this.layout.margin.top = Math.max(Math.round(+top), 0); } - if (!isNaN(right) && right >= 0){ this.layout.margin.right = Math.max(Math.round(+right), 0); } - if (!isNaN(bottom) && bottom >= 0){ this.layout.margin.bottom = Math.max(Math.round(+bottom), 0); } - if (!isNaN(left) && left >= 0){ this.layout.margin.left = Math.max(Math.round(+left), 0); } - if (this.layout.margin.top + this.layout.margin.bottom > this.layout.height){ - extra = Math.floor(((this.layout.margin.top + this.layout.margin.bottom) - this.layout.height) / 2); - this.layout.margin.top -= extra; - this.layout.margin.bottom -= extra; - } - if (this.layout.margin.left + this.layout.margin.right > this.layout.width){ - extra = Math.floor(((this.layout.margin.left + this.layout.margin.right) - this.layout.width) / 2); - this.layout.margin.left -= extra; - this.layout.margin.right -= extra; - } - ["top", "right", "bottom", "left"].forEach(function(m){ - this.layout.margin[m] = Math.max(this.layout.margin[m], 0); - }.bind(this)); - this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0); - this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0); - this.layout.cliparea.origin.x = this.layout.margin.left; - this.layout.cliparea.origin.y = this.layout.margin.top; - - if (this.initialized){ this.render(); } - return this; -}; - -/** + LocusZoom.Panel.prototype.setMargin = function (top, right, bottom, left) { + var extra; + if (!isNaN(top) && top >= 0) { + this.layout.margin.top = Math.max(Math.round(+top), 0); + } + if (!isNaN(right) && right >= 0) { + this.layout.margin.right = Math.max(Math.round(+right), 0); + } + if (!isNaN(bottom) && bottom >= 0) { + this.layout.margin.bottom = Math.max(Math.round(+bottom), 0); + } + if (!isNaN(left) && left >= 0) { + this.layout.margin.left = Math.max(Math.round(+left), 0); + } + if (this.layout.margin.top + this.layout.margin.bottom > this.layout.height) { + extra = Math.floor((this.layout.margin.top + this.layout.margin.bottom - this.layout.height) / 2); + this.layout.margin.top -= extra; + this.layout.margin.bottom -= extra; + } + if (this.layout.margin.left + this.layout.margin.right > this.layout.width) { + extra = Math.floor((this.layout.margin.left + this.layout.margin.right - this.layout.width) / 2); + this.layout.margin.left -= extra; + this.layout.margin.right -= extra; + } + [ + 'top', + 'right', + 'bottom', + 'left' + ].forEach(function (m) { + this.layout.margin[m] = Math.max(this.layout.margin[m], 0); + }.bind(this)); + this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0); + this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0); + this.layout.cliparea.origin.x = this.layout.margin.left; + this.layout.cliparea.origin.y = this.layout.margin.top; + if (this.initialized) { + this.render(); + } + return this; + }; + /** * Set the title for the panel. If passed an object, will merge the object with the existing layout configuration, so * that all or only some of the title layout object's parameters can be customized. If passed null, false, or an empty * string, the title DOM element will be set to display: none. @@ -10007,368 +9946,323 @@ LocusZoom.Panel.prototype.setMargin = function(top, right, bottom, left){ * @param {object} title.style CSS styles object to be applied to the title's DOM element. * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.setTitle = function(title){ - if (typeof this.layout.title == "string"){ - var text = this.layout.title; - this.layout.title = { text: text, x: 0, y: 0, style: {} }; - } - if (typeof title == "string"){ - this.layout.title.text = title; - } else if (typeof title == "object" && title !== null){ - this.layout.title = LocusZoom.Layouts.merge(title, this.layout.title); - } - if (this.layout.title.text.length){ - this.title.attr("display", null) - .attr("x", parseFloat(this.layout.title.x)) - .attr("y", parseFloat(this.layout.title.y)) - .style(this.layout.title.style) - .text(this.layout.title.text); - } else { - this.title.attr("display", "none"); - } - return this; -}; - - -/** + LocusZoom.Panel.prototype.setTitle = function (title) { + if (typeof this.layout.title == 'string') { + var text = this.layout.title; + this.layout.title = { + text: text, + x: 0, + y: 0, + style: {} + }; + } + if (typeof title == 'string') { + this.layout.title.text = title; + } else if (typeof title == 'object' && title !== null) { + this.layout.title = LocusZoom.Layouts.merge(title, this.layout.title); + } + if (this.layout.title.text.length) { + this.title.attr('display', null).attr('x', parseFloat(this.layout.title.x)).attr('y', parseFloat(this.layout.title.y)).style(this.layout.title.style).text(this.layout.title.text); + } else { + this.title.attr('display', 'none'); + } + return this; + }; + /** * Prepare the first rendering of the panel. This includes drawing the individual data layers, but also creates shared * elements such as axes, title, and loader/curtain. * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.initialize = function(){ - - // Append a container group element to house the main panel group element and the clip path - // Position with initial layout parameters - this.svg.container = this.parent.svg.append("g") - .attr("id", this.getBaseId() + ".panel_container") - .attr("transform", "translate(" + (this.layout.origin.x || 0) + "," + (this.layout.origin.y || 0) + ")"); - - // Append clip path to the parent svg element, size with initial layout parameters - var clipPath = this.svg.container.append("clipPath") - .attr("id", this.getBaseId() + ".clip"); - this.svg.clipRect = clipPath.append("rect") - .attr("width", this.layout.width).attr("height", this.layout.height); - - // Append svg group for rendering all panel child elements, clipped by the clip path - this.svg.group = this.svg.container.append("g") - .attr("id", this.getBaseId() + ".panel") - .attr("clip-path", "url(#" + this.getBaseId() + ".clip)"); - - // Add curtain and loader prototypes to the panel - /** @member {Object} */ - this.curtain = LocusZoom.generateCurtain.call(this); - /** @member {Object} */ - this.loader = LocusZoom.generateLoader.call(this); - - /** + LocusZoom.Panel.prototype.initialize = function () { + // Append a container group element to house the main panel group element and the clip path + // Position with initial layout parameters + this.svg.container = this.parent.svg.append('g').attr('id', this.getBaseId() + '.panel_container').attr('transform', 'translate(' + (this.layout.origin.x || 0) + ',' + (this.layout.origin.y || 0) + ')'); + // Append clip path to the parent svg element, size with initial layout parameters + var clipPath = this.svg.container.append('clipPath').attr('id', this.getBaseId() + '.clip'); + this.svg.clipRect = clipPath.append('rect').attr('width', this.layout.width).attr('height', this.layout.height); + // Append svg group for rendering all panel child elements, clipped by the clip path + this.svg.group = this.svg.container.append('g').attr('id', this.getBaseId() + '.panel').attr('clip-path', 'url(#' + this.getBaseId() + '.clip)'); + // Add curtain and loader prototypes to the panel + /** @member {Object} */ + this.curtain = LocusZoom.generateCurtain.call(this); + /** @member {Object} */ + this.loader = LocusZoom.generateLoader.call(this); + /** * Create the dashboard object and hang components on it as defined by panel layout * @member {LocusZoom.Dashboard} */ - this.dashboard = new LocusZoom.Dashboard(this); - - // Inner border - this.inner_border = this.svg.group.append("rect") - .attr("class", "lz-panel-background") - .on("click", function(){ - if (this.layout.background_click === "clear_selections"){ this.clearSelections(); } - }.bind(this)); - - // Add the title - /** @member {Element} */ - this.title = this.svg.group.append("text").attr("class", "lz-panel-title"); - if (typeof this.layout.title != "undefined"){ this.setTitle(); } - - // Initialize Axes - this.svg.x_axis = this.svg.group.append("g") - .attr("id", this.getBaseId() + ".x_axis").attr("class", "lz-x lz-axis"); - if (this.layout.axes.x.render){ - this.svg.x_axis_label = this.svg.x_axis.append("text") - .attr("class", "lz-x lz-axis lz-label") - .attr("text-anchor", "middle"); - } - this.svg.y1_axis = this.svg.group.append("g") - .attr("id", this.getBaseId() + ".y1_axis").attr("class", "lz-y lz-y1 lz-axis"); - if (this.layout.axes.y1.render){ - this.svg.y1_axis_label = this.svg.y1_axis.append("text") - .attr("class", "lz-y1 lz-axis lz-label") - .attr("text-anchor", "middle"); - } - this.svg.y2_axis = this.svg.group.append("g") - .attr("id", this.getBaseId() + ".y2_axis").attr("class", "lz-y lz-y2 lz-axis"); - if (this.layout.axes.y2.render){ - this.svg.y2_axis_label = this.svg.y2_axis.append("text") - .attr("class", "lz-y2 lz-axis lz-label") - .attr("text-anchor", "middle"); - } - - // Initialize child Data Layers - this.data_layer_ids_by_z_index.forEach(function(id){ - this.data_layers[id].initialize(); - }.bind(this)); - - /** + this.dashboard = new LocusZoom.Dashboard(this); + // Inner border + this.inner_border = this.svg.group.append('rect').attr('class', 'lz-panel-background').on('click', function () { + if (this.layout.background_click === 'clear_selections') { + this.clearSelections(); + } + }.bind(this)); + // Add the title + /** @member {Element} */ + this.title = this.svg.group.append('text').attr('class', 'lz-panel-title'); + if (typeof this.layout.title != 'undefined') { + this.setTitle(); + } + // Initialize Axes + this.svg.x_axis = this.svg.group.append('g').attr('id', this.getBaseId() + '.x_axis').attr('class', 'lz-x lz-axis'); + if (this.layout.axes.x.render) { + this.svg.x_axis_label = this.svg.x_axis.append('text').attr('class', 'lz-x lz-axis lz-label').attr('text-anchor', 'middle'); + } + this.svg.y1_axis = this.svg.group.append('g').attr('id', this.getBaseId() + '.y1_axis').attr('class', 'lz-y lz-y1 lz-axis'); + if (this.layout.axes.y1.render) { + this.svg.y1_axis_label = this.svg.y1_axis.append('text').attr('class', 'lz-y1 lz-axis lz-label').attr('text-anchor', 'middle'); + } + this.svg.y2_axis = this.svg.group.append('g').attr('id', this.getBaseId() + '.y2_axis').attr('class', 'lz-y lz-y2 lz-axis'); + if (this.layout.axes.y2.render) { + this.svg.y2_axis_label = this.svg.y2_axis.append('text').attr('class', 'lz-y2 lz-axis lz-label').attr('text-anchor', 'middle'); + } + // Initialize child Data Layers + this.data_layer_ids_by_z_index.forEach(function (id) { + this.data_layers[id].initialize(); + }.bind(this)); + /** * Legend object, as defined by panel layout and child data layer layouts * @member {LocusZoom.Legend} * */ - this.legend = null; - if (this.layout.legend){ - this.legend = new LocusZoom.Legend(this); - } - - // Establish panel background drag interaction mousedown event handler (on the panel background) - if (this.layout.interaction.drag_background_to_pan){ - var namespace = "." + this.parent.id + "." + this.id + ".interaction.drag"; - var mousedown = function(){ - this.parent.startDrag(this, "background"); - }.bind(this); - this.svg.container.select(".lz-panel-background") - .on("mousedown" + namespace + ".background", mousedown) - .on("touchstart" + namespace + ".background", mousedown); - } - - return this; - -}; - -/** + this.legend = null; + if (this.layout.legend) { + this.legend = new LocusZoom.Legend(this); + } + // Establish panel background drag interaction mousedown event handler (on the panel background) + if (this.layout.interaction.drag_background_to_pan) { + var namespace = '.' + this.parent.id + '.' + this.id + '.interaction.drag'; + var mousedown = function () { + this.parent.startDrag(this, 'background'); + }.bind(this); + this.svg.container.select('.lz-panel-background').on('mousedown' + namespace + '.background', mousedown).on('touchstart' + namespace + '.background', mousedown); + } + return this; + }; + /** * Refresh the sort order of all data layers (called by data layer moveUp and moveDown methods) */ -LocusZoom.Panel.prototype.resortDataLayers = function(){ - var sort = []; - this.data_layer_ids_by_z_index.forEach(function(id){ - sort.push(this.data_layers[id].layout.z_index); - }.bind(this)); - this.svg.group.selectAll("g.lz-data_layer-container").data(sort).sort(d3.ascending); - this.applyDataLayerZIndexesToDataLayerLayouts(); -}; - -/** + LocusZoom.Panel.prototype.resortDataLayers = function () { + var sort = []; + this.data_layer_ids_by_z_index.forEach(function (id) { + sort.push(this.data_layers[id].layout.z_index); + }.bind(this)); + this.svg.group.selectAll('g.lz-data_layer-container').data(sort).sort(d3.ascending); + this.applyDataLayerZIndexesToDataLayerLayouts(); + }; + /** * Get an array of panel IDs that are axis-linked to this panel * @param {('x'|'y1'|'y2')} axis * @returns {Array} */ -LocusZoom.Panel.prototype.getLinkedPanelIds = function(axis){ - axis = axis || null; - var linked_panel_ids = []; - if (["x","y1","y2"].indexOf(axis) === -1){ return linked_panel_ids; } - if (!this.layout.interaction[axis + "_linked"]){ return linked_panel_ids; } - this.parent.panel_ids_by_y_index.forEach(function(panel_id){ - if (panel_id !== this.id && this.parent.panels[panel_id].layout.interaction[axis + "_linked"]){ - linked_panel_ids.push(panel_id); - } - }.bind(this)); - return linked_panel_ids; -}; - -/** + LocusZoom.Panel.prototype.getLinkedPanelIds = function (axis) { + axis = axis || null; + var linked_panel_ids = []; + if ([ + 'x', + 'y1', + 'y2' + ].indexOf(axis) === -1) { + return linked_panel_ids; + } + if (!this.layout.interaction[axis + '_linked']) { + return linked_panel_ids; + } + this.parent.panel_ids_by_y_index.forEach(function (panel_id) { + if (panel_id !== this.id && this.parent.panels[panel_id].layout.interaction[axis + '_linked']) { + linked_panel_ids.push(panel_id); + } + }.bind(this)); + return linked_panel_ids; + }; + /** * Move a panel up relative to others by y-index * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.moveUp = function(){ - if (this.parent.panel_ids_by_y_index[this.layout.y_index - 1]){ - this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index - 1]; - this.parent.panel_ids_by_y_index[this.layout.y_index - 1] = this.id; - this.parent.applyPanelYIndexesToPanelLayouts(); - this.parent.positionPanels(); - } - return this; -}; - -/** + LocusZoom.Panel.prototype.moveUp = function () { + if (this.parent.panel_ids_by_y_index[this.layout.y_index - 1]) { + this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index - 1]; + this.parent.panel_ids_by_y_index[this.layout.y_index - 1] = this.id; + this.parent.applyPanelYIndexesToPanelLayouts(); + this.parent.positionPanels(); + } + return this; + }; + /** * Move a panel down (y-axis) relative to others in the plot * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.moveDown = function(){ - if (this.parent.panel_ids_by_y_index[this.layout.y_index + 1]){ - this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index + 1]; - this.parent.panel_ids_by_y_index[this.layout.y_index + 1] = this.id; - this.parent.applyPanelYIndexesToPanelLayouts(); - this.parent.positionPanels(); - } - return this; -}; - -/** + LocusZoom.Panel.prototype.moveDown = function () { + if (this.parent.panel_ids_by_y_index[this.layout.y_index + 1]) { + this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index + 1]; + this.parent.panel_ids_by_y_index[this.layout.y_index + 1] = this.id; + this.parent.applyPanelYIndexesToPanelLayouts(); + this.parent.positionPanels(); + } + return this; + }; + /** * Create a new data layer from a provided layout object. Should have the keys specified in `DefaultLayout` * Will automatically add at the top (depth/z-index) of the panel unless explicitly directed differently * in the layout provided. * @param {object} layout * @returns {*} */ -LocusZoom.Panel.prototype.addDataLayer = function(layout){ - - // Sanity checks - if (typeof layout !== "object" || typeof layout.id !== "string" || !layout.id.length){ - throw "Invalid data layer layout passed to LocusZoom.Panel.prototype.addDataLayer()"; - } - if (typeof this.data_layers[layout.id] !== "undefined"){ - throw "Cannot create data_layer with id [" + layout.id + "]; data layer with that id already exists in the panel"; - } - if (typeof layout.type !== "string"){ - throw "Invalid data layer type in layout passed to LocusZoom.Panel.prototype.addDataLayer()"; - } - - // If the layout defines a y axis make sure the axis number is set and is 1 or 2 (default to 1) - if (typeof layout.y_axis == "object" && (typeof layout.y_axis.axis == "undefined" || [1,2].indexOf(layout.y_axis.axis) === -1)){ - layout.y_axis.axis = 1; - } - - // Create the Data Layer - var data_layer = LocusZoom.DataLayers.get(layout.type, layout, this); - - // Store the Data Layer on the Panel - this.data_layers[data_layer.id] = data_layer; - - // If a discrete z_index was set in the layout then adjust other data layer z_index values to accommodate this one - if (data_layer.layout.z_index !== null && !isNaN(data_layer.layout.z_index) - && this.data_layer_ids_by_z_index.length > 0){ - // Negative z_index values should count backwards from the end, so convert negatives to appropriate values here - if (data_layer.layout.z_index < 0){ - data_layer.layout.z_index = Math.max(this.data_layer_ids_by_z_index.length + data_layer.layout.z_index, 0); - } - this.data_layer_ids_by_z_index.splice(data_layer.layout.z_index, 0, data_layer.id); - this.data_layer_ids_by_z_index.forEach(function(dlid, idx){ - this.data_layers[dlid].layout.z_index = idx; - }.bind(this)); - } else { - var length = this.data_layer_ids_by_z_index.push(data_layer.id); - this.data_layers[data_layer.id].layout.z_index = length - 1; - } - - // Determine if this data layer was already in the layout.data_layers array. - // If it wasn't, add it. Either way store the layout.data_layers array index on the data_layer. - var layout_idx = null; - this.layout.data_layers.forEach(function(data_layer_layout, idx){ - if (data_layer_layout.id === data_layer.id){ layout_idx = idx; } - }); - if (layout_idx === null){ - layout_idx = this.layout.data_layers.push(this.data_layers[data_layer.id].layout) - 1; - } - this.data_layers[data_layer.id].layout_idx = layout_idx; - - return this.data_layers[data_layer.id]; -}; - -/** + LocusZoom.Panel.prototype.addDataLayer = function (layout) { + // Sanity checks + if (typeof layout !== 'object' || typeof layout.id !== 'string' || !layout.id.length) { + throw 'Invalid data layer layout passed to LocusZoom.Panel.prototype.addDataLayer()'; + } + if (typeof this.data_layers[layout.id] !== 'undefined') { + throw 'Cannot create data_layer with id [' + layout.id + ']; data layer with that id already exists in the panel'; + } + if (typeof layout.type !== 'string') { + throw 'Invalid data layer type in layout passed to LocusZoom.Panel.prototype.addDataLayer()'; + } + // If the layout defines a y axis make sure the axis number is set and is 1 or 2 (default to 1) + if (typeof layout.y_axis == 'object' && (typeof layout.y_axis.axis == 'undefined' || [ + 1, + 2 + ].indexOf(layout.y_axis.axis) === -1)) { + layout.y_axis.axis = 1; + } + // Create the Data Layer + var data_layer = LocusZoom.DataLayers.get(layout.type, layout, this); + // Store the Data Layer on the Panel + this.data_layers[data_layer.id] = data_layer; + // If a discrete z_index was set in the layout then adjust other data layer z_index values to accommodate this one + if (data_layer.layout.z_index !== null && !isNaN(data_layer.layout.z_index) && this.data_layer_ids_by_z_index.length > 0) { + // Negative z_index values should count backwards from the end, so convert negatives to appropriate values here + if (data_layer.layout.z_index < 0) { + data_layer.layout.z_index = Math.max(this.data_layer_ids_by_z_index.length + data_layer.layout.z_index, 0); + } + this.data_layer_ids_by_z_index.splice(data_layer.layout.z_index, 0, data_layer.id); + this.data_layer_ids_by_z_index.forEach(function (dlid, idx) { + this.data_layers[dlid].layout.z_index = idx; + }.bind(this)); + } else { + var length = this.data_layer_ids_by_z_index.push(data_layer.id); + this.data_layers[data_layer.id].layout.z_index = length - 1; + } + // Determine if this data layer was already in the layout.data_layers array. + // If it wasn't, add it. Either way store the layout.data_layers array index on the data_layer. + var layout_idx = null; + this.layout.data_layers.forEach(function (data_layer_layout, idx) { + if (data_layer_layout.id === data_layer.id) { + layout_idx = idx; + } + }); + if (layout_idx === null) { + layout_idx = this.layout.data_layers.push(this.data_layers[data_layer.id].layout) - 1; + } + this.data_layers[data_layer.id].layout_idx = layout_idx; + return this.data_layers[data_layer.id]; + }; + /** * Remove a data layer by id * @param {string} id * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.removeDataLayer = function(id){ - if (!this.data_layers[id]){ - throw ("Unable to remove data layer, ID not found: " + id); - } - - // Destroy all tooltips for the data layer - this.data_layers[id].destroyAllTooltips(); - - // Remove the svg container for the data layer if it exists - if (this.data_layers[id].svg.container){ - this.data_layers[id].svg.container.remove(); - } - - // Delete the data layer and its presence in the panel layout and state - this.layout.data_layers.splice(this.data_layers[id].layout_idx, 1); - delete this.state[this.data_layers[id].state_id]; - delete this.data_layers[id]; - - // Remove the data_layer id from the z_index array - this.data_layer_ids_by_z_index.splice(this.data_layer_ids_by_z_index.indexOf(id), 1); - - // Update layout_idx and layout.z_index values for all remaining data_layers - this.applyDataLayerZIndexesToDataLayerLayouts(); - this.layout.data_layers.forEach(function(data_layer_layout, idx){ - this.data_layers[data_layer_layout.id].layout_idx = idx; - }.bind(this)); - - return this; -}; - -/** + LocusZoom.Panel.prototype.removeDataLayer = function (id) { + if (!this.data_layers[id]) { + throw 'Unable to remove data layer, ID not found: ' + id; + } + // Destroy all tooltips for the data layer + this.data_layers[id].destroyAllTooltips(); + // Remove the svg container for the data layer if it exists + if (this.data_layers[id].svg.container) { + this.data_layers[id].svg.container.remove(); + } + // Delete the data layer and its presence in the panel layout and state + this.layout.data_layers.splice(this.data_layers[id].layout_idx, 1); + delete this.state[this.data_layers[id].state_id]; + delete this.data_layers[id]; + // Remove the data_layer id from the z_index array + this.data_layer_ids_by_z_index.splice(this.data_layer_ids_by_z_index.indexOf(id), 1); + // Update layout_idx and layout.z_index values for all remaining data_layers + this.applyDataLayerZIndexesToDataLayerLayouts(); + this.layout.data_layers.forEach(function (data_layer_layout, idx) { + this.data_layers[data_layer_layout.id].layout_idx = idx; + }.bind(this)); + return this; + }; + /** * Clear all selections on all data layers * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.clearSelections = function(){ - this.data_layer_ids_by_z_index.forEach(function(id){ - this.data_layers[id].setAllElementStatus("selected", false); - }.bind(this)); - return this; -}; - -/** + LocusZoom.Panel.prototype.clearSelections = function () { + this.data_layer_ids_by_z_index.forEach(function (id) { + this.data_layers[id].setAllElementStatus('selected', false); + }.bind(this)); + return this; + }; + /** * When the parent plot changes state, adjust the panel accordingly. For example, this may include fetching new data * from the API as the viewing region changes * @returns {Promise} */ -LocusZoom.Panel.prototype.reMap = function(){ - this.emit("data_requested"); - this.data_promises = []; - - // Remove any previous error messages before attempting to load new data - this.curtain.hide(); - // Trigger reMap on each Data Layer - for (var id in this.data_layers){ - try { - this.data_promises.push(this.data_layers[id].reMap()); - } catch (error) { - console.warn(error); - this.curtain.show(error); - } - } - // When all finished trigger a render - return Q.all(this.data_promises) - .then(function(){ - this.initialized = true; - this.render(); - this.emit("layout_changed"); - this.parent.emit("layout_changed"); - this.emit("data_rendered"); - }.bind(this)) - .catch(function(error){ - console.warn(error); - this.curtain.show(error); - }.bind(this)); -}; - -/** + LocusZoom.Panel.prototype.reMap = function () { + this.emit('data_requested'); + this.data_promises = []; + // Remove any previous error messages before attempting to load new data + this.curtain.hide(); + // Trigger reMap on each Data Layer + for (var id in this.data_layers) { + try { + this.data_promises.push(this.data_layers[id].reMap()); + } catch (error) { + console.warn(error); + this.curtain.show(error); + } + } + // When all finished trigger a render + return Q.all(this.data_promises).then(function () { + this.initialized = true; + this.render(); + this.emit('layout_changed'); + this.parent.emit('layout_changed'); + this.emit('data_rendered'); + }.bind(this)).catch(function (error) { + console.warn(error); + this.curtain.show(error); + }.bind(this)); + }; + /** * Iterate over data layers to generate panel axis extents * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.generateExtents = function(){ - - // Reset extents - ["x", "y1", "y2"].forEach(function(axis){ - this[axis + "_extent"] = null; - }.bind(this)); - - // Loop through the data layers - for (var id in this.data_layers){ - - var data_layer = this.data_layers[id]; - - // If defined and not decoupled, merge the x extent of the data layer with the panel's x extent - if (data_layer.layout.x_axis && !data_layer.layout.x_axis.decoupled){ - this.x_extent = d3.extent((this.x_extent || []).concat(data_layer.getAxisExtent("x"))); - } - - // If defined and not decoupled, merge the y extent of the data layer with the panel's appropriate y extent - if (data_layer.layout.y_axis && !data_layer.layout.y_axis.decoupled){ - var y_axis = "y" + data_layer.layout.y_axis.axis; - this[y_axis+"_extent"] = d3.extent((this[y_axis+"_extent"] || []).concat(data_layer.getAxisExtent("y"))); - } - - } - - // Override x_extent from state if explicitly defined to do so - if (this.layout.axes.x && this.layout.axes.x.extent === "state"){ - this.x_extent = [ this.state.start, this.state.end ]; - } - - return this; - -}; - -/** + LocusZoom.Panel.prototype.generateExtents = function () { + // Reset extents + [ + 'x', + 'y1', + 'y2' + ].forEach(function (axis) { + this[axis + '_extent'] = null; + }.bind(this)); + // Loop through the data layers + for (var id in this.data_layers) { + var data_layer = this.data_layers[id]; + // If defined and not decoupled, merge the x extent of the data layer with the panel's x extent + if (data_layer.layout.x_axis && !data_layer.layout.x_axis.decoupled) { + this.x_extent = d3.extent((this.x_extent || []).concat(data_layer.getAxisExtent('x'))); + } + // If defined and not decoupled, merge the y extent of the data layer with the panel's appropriate y extent + if (data_layer.layout.y_axis && !data_layer.layout.y_axis.decoupled) { + var y_axis = 'y' + data_layer.layout.y_axis.axis; + this[y_axis + '_extent'] = d3.extent((this[y_axis + '_extent'] || []).concat(data_layer.getAxisExtent('y'))); + } + } + // Override x_extent from state if explicitly defined to do so + if (this.layout.axes.x && this.layout.axes.x.extent === 'state') { + this.x_extent = [ + this.state.start, + this.state.end + ]; + } + return this; + }; + /** * Generate an array of ticks for an axis. These ticks are generated in one of three ways (highest wins): * 1. An array of specific tick marks * 2. Query each data layer for what ticks are appropriate, and allow a panel-level tick configuration parameter @@ -10384,461 +10278,483 @@ LocusZoom.Panel.prototype.generateExtents = function(){ * * transform: SVG transform attribute string * * color: string or LocusZoom scalable parameter object */ -LocusZoom.Panel.prototype.generateTicks = function(axis){ - - // Parse an explicit 'ticks' attribute in the axis layout - if (this.layout.axes[axis].ticks){ - var layout = this.layout.axes[axis]; - - var baseTickConfig = layout.ticks; - if (Array.isArray(baseTickConfig)){ - // Array of specific ticks hard-coded into a panel will override any ticks that an individual layer might specify - return baseTickConfig; - } - - if (typeof baseTickConfig === "object") { - // If the layout specifies base configuration for ticks- but without specific positions- then ask each - // data layer to report the tick marks that it thinks it needs - // TODO: Few layers currently need to specify custom ticks (which is ok!). But if it becomes common, consider adding mechanisms to deduplicate ticks across layers - var self = this; - - // Pass any layer-specific customizations for how ticks are calculated. (styles are overridden separately) - var config = { position: baseTickConfig.position }; - - var combinedTicks = this.data_layer_ids_by_z_index.reduce(function(acc, data_layer_id) { - var nextLayer = self.data_layers[data_layer_id]; - return acc.concat(nextLayer.getTicks(axis, config)); - }, []); - - return combinedTicks.map(function(item) { - // The layer makes suggestions, but tick configuration params specified on the panel take precedence - var itemConfig = {}; - itemConfig = LocusZoom.Layouts.merge(itemConfig, baseTickConfig); - return LocusZoom.Layouts.merge(itemConfig, item); - }); - } - } - - // If no other configuration is provided, attempt to generate ticks from the extent - if (this[axis + "_extent"]) { - return LocusZoom.prettyTicks(this[axis + "_extent"], "both"); - } - return []; -}; - -/** + LocusZoom.Panel.prototype.generateTicks = function (axis) { + // Parse an explicit 'ticks' attribute in the axis layout + if (this.layout.axes[axis].ticks) { + var layout = this.layout.axes[axis]; + var baseTickConfig = layout.ticks; + if (Array.isArray(baseTickConfig)) { + // Array of specific ticks hard-coded into a panel will override any ticks that an individual layer might specify + return baseTickConfig; + } + if (typeof baseTickConfig === 'object') { + // If the layout specifies base configuration for ticks- but without specific positions- then ask each + // data layer to report the tick marks that it thinks it needs + // TODO: Few layers currently need to specify custom ticks (which is ok!). But if it becomes common, consider adding mechanisms to deduplicate ticks across layers + var self = this; + // Pass any layer-specific customizations for how ticks are calculated. (styles are overridden separately) + var config = { position: baseTickConfig.position }; + var combinedTicks = this.data_layer_ids_by_z_index.reduce(function (acc, data_layer_id) { + var nextLayer = self.data_layers[data_layer_id]; + return acc.concat(nextLayer.getTicks(axis, config)); + }, []); + return combinedTicks.map(function (item) { + // The layer makes suggestions, but tick configuration params specified on the panel take precedence + var itemConfig = {}; + itemConfig = LocusZoom.Layouts.merge(itemConfig, baseTickConfig); + return LocusZoom.Layouts.merge(itemConfig, item); + }); + } + } + // If no other configuration is provided, attempt to generate ticks from the extent + if (this[axis + '_extent']) { + return LocusZoom.prettyTicks(this[axis + '_extent'], 'both'); + } + return []; + }; + /** * Update rendering of this panel whenever an event triggers a redraw. Assumes that the panel has already been * prepared the first time via `initialize` * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.render = function(){ - - // Position the panel container - this.svg.container.attr("transform", "translate(" + this.layout.origin.x + "," + this.layout.origin.y + ")"); - - // Set size on the clip rect - this.svg.clipRect.attr("width", this.layout.width).attr("height", this.layout.height); - - // Set and position the inner border, style if necessary - this.inner_border - .attr("x", this.layout.margin.left).attr("y", this.layout.margin.top) - .attr("width", this.layout.width - (this.layout.margin.left + this.layout.margin.right)) - .attr("height", this.layout.height - (this.layout.margin.top + this.layout.margin.bottom)); - if (this.layout.inner_border){ - this.inner_border.style({ "stroke-width": 1, "stroke": this.layout.inner_border }); - } - - // Set/update panel title if necessary - this.setTitle(); - - // Regenerate all extents - this.generateExtents(); - - // Helper function to constrain any procedurally generated vectors (e.g. ranges, extents) - // Constraints applied here keep vectors from going to infinity or beyond a definable power of ten - var constrain = function(value, limit_exponent){ - var neg_min = Math.pow(-10, limit_exponent); - var neg_max = Math.pow(-10, -limit_exponent); - var pos_min = Math.pow(10, -limit_exponent); - var pos_max = Math.pow(10, limit_exponent); - if (value === Infinity){ value = pos_max; } - if (value === -Infinity){ value = neg_min; } - if (value === 0){ value = pos_min; } - if (value > 0){ value = Math.max(Math.min(value, pos_max), pos_min); } - if (value < 0){ value = Math.max(Math.min(value, neg_max), neg_min); } - return value; - }; - - // Define default and shifted ranges for all axes - var ranges = {}; - if (this.x_extent){ - var base_x_range = { start: 0, end: this.layout.cliparea.width }; - if (this.layout.axes.x.range){ - base_x_range.start = this.layout.axes.x.range.start || base_x_range.start; - base_x_range.end = this.layout.axes.x.range.end || base_x_range.end; - } - ranges.x = [base_x_range.start, base_x_range.end]; - ranges.x_shifted = [base_x_range.start, base_x_range.end]; - } - if (this.y1_extent){ - var base_y1_range = { start: this.layout.cliparea.height, end: 0 }; - if (this.layout.axes.y1.range){ - base_y1_range.start = this.layout.axes.y1.range.start || base_y1_range.start; - base_y1_range.end = this.layout.axes.y1.range.end || base_y1_range.end; - } - ranges.y1 = [base_y1_range.start, base_y1_range.end]; - ranges.y1_shifted = [base_y1_range.start, base_y1_range.end]; - } - if (this.y2_extent){ - var base_y2_range = { start: this.layout.cliparea.height, end: 0 }; - if (this.layout.axes.y2.range){ - base_y2_range.start = this.layout.axes.y2.range.start || base_y2_range.start; - base_y2_range.end = this.layout.axes.y2.range.end || base_y2_range.end; - } - ranges.y2 = [base_y2_range.start, base_y2_range.end]; - ranges.y2_shifted = [base_y2_range.start, base_y2_range.end]; - } - - // Shift ranges based on any drag or zoom interactions currently underway - if (this.parent.interaction.panel_id && (this.parent.interaction.panel_id === this.id || this.parent.interaction.linked_panel_ids.indexOf(this.id) !== -1)){ - var anchor, scalar = null; - if (this.parent.interaction.zooming && typeof this.x_scale == "function"){ - var current_extent_size = Math.abs(this.x_extent[1] - this.x_extent[0]); - var current_scaled_extent_size = Math.round(this.x_scale.invert(ranges.x_shifted[1])) - Math.round(this.x_scale.invert(ranges.x_shifted[0])); - var zoom_factor = this.parent.interaction.zooming.scale; - var potential_extent_size = Math.floor(current_scaled_extent_size * (1 / zoom_factor)); - if (zoom_factor < 1 && !isNaN(this.parent.layout.max_region_scale)){ - zoom_factor = 1 /(Math.min(potential_extent_size, this.parent.layout.max_region_scale) / current_scaled_extent_size); - } else if (zoom_factor > 1 && !isNaN(this.parent.layout.min_region_scale)){ - zoom_factor = 1 / (Math.max(potential_extent_size, this.parent.layout.min_region_scale) / current_scaled_extent_size); - } - var new_extent_size = Math.floor(current_extent_size * zoom_factor); - anchor = this.parent.interaction.zooming.center - this.layout.margin.left - this.layout.origin.x; - var offset_ratio = anchor / this.layout.cliparea.width; - var new_x_extent_start = Math.max(Math.floor(this.x_scale.invert(ranges.x_shifted[0]) - ((new_extent_size - current_scaled_extent_size) * offset_ratio)), 1); - ranges.x_shifted = [ this.x_scale(new_x_extent_start), this.x_scale(new_x_extent_start + new_extent_size) ]; - } else if (this.parent.interaction.dragging){ - switch (this.parent.interaction.dragging.method){ - case "background": - ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x; - ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x; - break; - case "x_tick": - if (d3.event && d3.event.shiftKey){ - ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x; - ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x; - } else { - anchor = this.parent.interaction.dragging.start_x - this.layout.margin.left - this.layout.origin.x; - scalar = constrain(anchor / (anchor + this.parent.interaction.dragging.dragged_x), 3); - ranges.x_shifted[0] = 0; - ranges.x_shifted[1] = Math.max(this.layout.cliparea.width * (1 / scalar), 1); + LocusZoom.Panel.prototype.render = function () { + // Position the panel container + this.svg.container.attr('transform', 'translate(' + this.layout.origin.x + ',' + this.layout.origin.y + ')'); + // Set size on the clip rect + this.svg.clipRect.attr('width', this.layout.width).attr('height', this.layout.height); + // Set and position the inner border, style if necessary + this.inner_border.attr('x', this.layout.margin.left).attr('y', this.layout.margin.top).attr('width', this.layout.width - (this.layout.margin.left + this.layout.margin.right)).attr('height', this.layout.height - (this.layout.margin.top + this.layout.margin.bottom)); + if (this.layout.inner_border) { + this.inner_border.style({ + 'stroke-width': 1, + 'stroke': this.layout.inner_border + }); + } + // Set/update panel title if necessary + this.setTitle(); + // Regenerate all extents + this.generateExtents(); + // Helper function to constrain any procedurally generated vectors (e.g. ranges, extents) + // Constraints applied here keep vectors from going to infinity or beyond a definable power of ten + var constrain = function (value, limit_exponent) { + var neg_min = Math.pow(-10, limit_exponent); + var neg_max = Math.pow(-10, -limit_exponent); + var pos_min = Math.pow(10, -limit_exponent); + var pos_max = Math.pow(10, limit_exponent); + if (value === Infinity) { + value = pos_max; } - break; - case "y1_tick": - case "y2_tick": - var y_shifted = "y" + this.parent.interaction.dragging.method[1] + "_shifted"; - if (d3.event && d3.event.shiftKey){ - ranges[y_shifted][0] = this.layout.cliparea.height + this.parent.interaction.dragging.dragged_y; - ranges[y_shifted][1] = +this.parent.interaction.dragging.dragged_y; - } else { - anchor = this.layout.cliparea.height - (this.parent.interaction.dragging.start_y - this.layout.margin.top - this.layout.origin.y); - scalar = constrain(anchor / (anchor - this.parent.interaction.dragging.dragged_y), 3); - ranges[y_shifted][0] = this.layout.cliparea.height; - ranges[y_shifted][1] = this.layout.cliparea.height - (this.layout.cliparea.height * (1 / scalar)); + if (value === -Infinity) { + value = neg_min; } + if (value === 0) { + value = pos_min; + } + if (value > 0) { + value = Math.max(Math.min(value, pos_max), pos_min); + } + if (value < 0) { + value = Math.max(Math.min(value, neg_max), neg_min); + } + return value; + }; + // Define default and shifted ranges for all axes + var ranges = {}; + if (this.x_extent) { + var base_x_range = { + start: 0, + end: this.layout.cliparea.width + }; + if (this.layout.axes.x.range) { + base_x_range.start = this.layout.axes.x.range.start || base_x_range.start; + base_x_range.end = this.layout.axes.x.range.end || base_x_range.end; + } + ranges.x = [ + base_x_range.start, + base_x_range.end + ]; + ranges.x_shifted = [ + base_x_range.start, + base_x_range.end + ]; } - } - } - - // Generate scales and ticks for all axes, then render them - ["x", "y1", "y2"].forEach(function(axis){ - if (!this[axis + "_extent"]){ return; } - - // Base Scale - this[axis + "_scale"] = d3.scale.linear() - .domain(this[axis + "_extent"]) - .range(ranges[axis + "_shifted"]); - - // Shift the extent - this[axis + "_extent"] = [ - this[axis + "_scale"].invert(ranges[axis][0]), - this[axis + "_scale"].invert(ranges[axis][1]) - ]; - - // Finalize Scale - this[axis + "_scale"] = d3.scale.linear() - .domain(this[axis + "_extent"]).range(ranges[axis]); - - // Render axis (and generate ticks as needed) - this.renderAxis(axis); - }.bind(this)); - - // Establish mousewheel zoom event handers on the panel (namespacing not passed through by d3, so not used here) - if (this.layout.interaction.scroll_to_zoom){ - var zoom_handler = function(){ - // Look for a shift key press while scrolling to execute. - // If not present, gracefully raise a notification and allow conventional scrolling - if (!d3.event.shiftKey){ - if (this.parent.canInteract(this.id)){ - this.loader.show("Press [SHIFT] while scrolling to zoom").hide(1000); + if (this.y1_extent) { + var base_y1_range = { + start: this.layout.cliparea.height, + end: 0 + }; + if (this.layout.axes.y1.range) { + base_y1_range.start = this.layout.axes.y1.range.start || base_y1_range.start; + base_y1_range.end = this.layout.axes.y1.range.end || base_y1_range.end; } - return; + ranges.y1 = [ + base_y1_range.start, + base_y1_range.end + ]; + ranges.y1_shifted = [ + base_y1_range.start, + base_y1_range.end + ]; } - d3.event.preventDefault(); - if (!this.parent.canInteract(this.id)){ return; } - var coords = d3.mouse(this.svg.container.node()); - var delta = Math.max(-1, Math.min(1, (d3.event.wheelDelta || -d3.event.detail || -d3.event.deltaY))); - if (delta === 0){ return; } - this.parent.interaction = { - panel_id: this.id, - linked_panel_ids: this.getLinkedPanelIds("x"), - zooming: { - scale: (delta < 1) ? 0.9 : 1.1, - center: coords[0] + if (this.y2_extent) { + var base_y2_range = { + start: this.layout.cliparea.height, + end: 0 + }; + if (this.layout.axes.y2.range) { + base_y2_range.start = this.layout.axes.y2.range.start || base_y2_range.start; + base_y2_range.end = this.layout.axes.y2.range.end || base_y2_range.end; } - }; - this.render(); - this.parent.interaction.linked_panel_ids.forEach(function(panel_id){ - this.parent.panels[panel_id].render(); + ranges.y2 = [ + base_y2_range.start, + base_y2_range.end + ]; + ranges.y2_shifted = [ + base_y2_range.start, + base_y2_range.end + ]; + } + // Shift ranges based on any drag or zoom interactions currently underway + if (this.parent.interaction.panel_id && (this.parent.interaction.panel_id === this.id || this.parent.interaction.linked_panel_ids.indexOf(this.id) !== -1)) { + var anchor, scalar = null; + if (this.parent.interaction.zooming && typeof this.x_scale == 'function') { + var current_extent_size = Math.abs(this.x_extent[1] - this.x_extent[0]); + var current_scaled_extent_size = Math.round(this.x_scale.invert(ranges.x_shifted[1])) - Math.round(this.x_scale.invert(ranges.x_shifted[0])); + var zoom_factor = this.parent.interaction.zooming.scale; + var potential_extent_size = Math.floor(current_scaled_extent_size * (1 / zoom_factor)); + if (zoom_factor < 1 && !isNaN(this.parent.layout.max_region_scale)) { + zoom_factor = 1 / (Math.min(potential_extent_size, this.parent.layout.max_region_scale) / current_scaled_extent_size); + } else if (zoom_factor > 1 && !isNaN(this.parent.layout.min_region_scale)) { + zoom_factor = 1 / (Math.max(potential_extent_size, this.parent.layout.min_region_scale) / current_scaled_extent_size); + } + var new_extent_size = Math.floor(current_extent_size * zoom_factor); + anchor = this.parent.interaction.zooming.center - this.layout.margin.left - this.layout.origin.x; + var offset_ratio = anchor / this.layout.cliparea.width; + var new_x_extent_start = Math.max(Math.floor(this.x_scale.invert(ranges.x_shifted[0]) - (new_extent_size - current_scaled_extent_size) * offset_ratio), 1); + ranges.x_shifted = [ + this.x_scale(new_x_extent_start), + this.x_scale(new_x_extent_start + new_extent_size) + ]; + } else if (this.parent.interaction.dragging) { + switch (this.parent.interaction.dragging.method) { + case 'background': + ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x; + ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x; + break; + case 'x_tick': + if (d3.event && d3.event.shiftKey) { + ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x; + ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x; + } else { + anchor = this.parent.interaction.dragging.start_x - this.layout.margin.left - this.layout.origin.x; + scalar = constrain(anchor / (anchor + this.parent.interaction.dragging.dragged_x), 3); + ranges.x_shifted[0] = 0; + ranges.x_shifted[1] = Math.max(this.layout.cliparea.width * (1 / scalar), 1); + } + break; + case 'y1_tick': + case 'y2_tick': + var y_shifted = 'y' + this.parent.interaction.dragging.method[1] + '_shifted'; + if (d3.event && d3.event.shiftKey) { + ranges[y_shifted][0] = this.layout.cliparea.height + this.parent.interaction.dragging.dragged_y; + ranges[y_shifted][1] = +this.parent.interaction.dragging.dragged_y; + } else { + anchor = this.layout.cliparea.height - (this.parent.interaction.dragging.start_y - this.layout.margin.top - this.layout.origin.y); + scalar = constrain(anchor / (anchor - this.parent.interaction.dragging.dragged_y), 3); + ranges[y_shifted][0] = this.layout.cliparea.height; + ranges[y_shifted][1] = this.layout.cliparea.height - this.layout.cliparea.height * (1 / scalar); + } + } + } + } + // Generate scales and ticks for all axes, then render them + [ + 'x', + 'y1', + 'y2' + ].forEach(function (axis) { + if (!this[axis + '_extent']) { + return; + } + // Base Scale + this[axis + '_scale'] = d3.scale.linear().domain(this[axis + '_extent']).range(ranges[axis + '_shifted']); + // Shift the extent + this[axis + '_extent'] = [ + this[axis + '_scale'].invert(ranges[axis][0]), + this[axis + '_scale'].invert(ranges[axis][1]) + ]; + // Finalize Scale + this[axis + '_scale'] = d3.scale.linear().domain(this[axis + '_extent']).range(ranges[axis]); + // Render axis (and generate ticks as needed) + this.renderAxis(axis); }.bind(this)); - if (this.zoom_timeout !== null){ clearTimeout(this.zoom_timeout); } - this.zoom_timeout = setTimeout(function(){ - this.parent.interaction = {}; - this.parent.applyState({ start: this.x_extent[0], end: this.x_extent[1] }); - }.bind(this), 500); - }.bind(this); - this.zoom_listener = d3.behavior.zoom(); - this.svg.container.call(this.zoom_listener) - .on("wheel.zoom", zoom_handler) - .on("mousewheel.zoom", zoom_handler) - .on("DOMMouseScroll.zoom", zoom_handler); - } - - // Render data layers in order by z-index - this.data_layer_ids_by_z_index.forEach(function(data_layer_id){ - this.data_layers[data_layer_id].draw().render(); - }.bind(this)); - - return this; -}; - - -/** + // Establish mousewheel zoom event handers on the panel (namespacing not passed through by d3, so not used here) + if (this.layout.interaction.scroll_to_zoom) { + var zoom_handler = function () { + // Look for a shift key press while scrolling to execute. + // If not present, gracefully raise a notification and allow conventional scrolling + if (!d3.event.shiftKey) { + if (this.parent.canInteract(this.id)) { + this.loader.show('Press [SHIFT] while scrolling to zoom').hide(1000); + } + return; + } + d3.event.preventDefault(); + if (!this.parent.canInteract(this.id)) { + return; + } + var coords = d3.mouse(this.svg.container.node()); + var delta = Math.max(-1, Math.min(1, d3.event.wheelDelta || -d3.event.detail || -d3.event.deltaY)); + if (delta === 0) { + return; + } + this.parent.interaction = { + panel_id: this.id, + linked_panel_ids: this.getLinkedPanelIds('x'), + zooming: { + scale: delta < 1 ? 0.9 : 1.1, + center: coords[0] + } + }; + this.render(); + this.parent.interaction.linked_panel_ids.forEach(function (panel_id) { + this.parent.panels[panel_id].render(); + }.bind(this)); + if (this.zoom_timeout !== null) { + clearTimeout(this.zoom_timeout); + } + this.zoom_timeout = setTimeout(function () { + this.parent.interaction = {}; + this.parent.applyState({ + start: this.x_extent[0], + end: this.x_extent[1] + }); + }.bind(this), 500); + }.bind(this); + this.zoom_listener = d3.behavior.zoom(); + this.svg.container.call(this.zoom_listener).on('wheel.zoom', zoom_handler).on('mousewheel.zoom', zoom_handler).on('DOMMouseScroll.zoom', zoom_handler); + } + // Render data layers in order by z-index + this.data_layer_ids_by_z_index.forEach(function (data_layer_id) { + this.data_layers[data_layer_id].draw().render(); + }.bind(this)); + return this; + }; + /** * Render ticks for a particular axis * @param {('x'|'y1'|'y2')} axis The identifier of the axes * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.renderAxis = function(axis){ - - if (["x", "y1", "y2"].indexOf(axis) === -1){ - throw("Unable to render axis; invalid axis identifier: " + axis); - } - - var canRender = this.layout.axes[axis].render - && typeof this[axis + "_scale"] == "function" - && !isNaN(this[axis + "_scale"](0)); - - // If the axis has already been rendered then check if we can/can't render it - // Make sure the axis element is shown/hidden to suit - if (this[axis+"_axis"]){ - this.svg.container.select("g.lz-axis.lz-"+axis).style("display", canRender ? null : "none"); - } - - if (!canRender){ return this; } - - // Axis-specific values to plug in where needed - var axis_params = { - x: { - position: "translate(" + this.layout.margin.left + "," + (this.layout.height - this.layout.margin.bottom) + ")", - orientation: "bottom", - label_x: this.layout.cliparea.width / 2, - label_y: (this.layout.axes[axis].label_offset || 0), - label_rotate: null - }, - y1: { - position: "translate(" + this.layout.margin.left + "," + this.layout.margin.top + ")", - orientation: "left", - label_x: -1 * (this.layout.axes[axis].label_offset || 0), - label_y: this.layout.cliparea.height / 2, - label_rotate: -90 - }, - y2: { - position: "translate(" + (this.layout.width - this.layout.margin.right) + "," + this.layout.margin.top + ")", - orientation: "right", - label_x: (this.layout.axes[axis].label_offset || 0), - label_y: this.layout.cliparea.height / 2, - label_rotate: -90 - } - }; - - // Generate Ticks - this[axis + "_ticks"] = this.generateTicks(axis); - - // Determine if the ticks are all numbers (d3-automated tick rendering) or not (manual tick rendering) - var ticksAreAllNumbers = (function(ticks){ - for (var i = 0; i < ticks.length; i++){ - if (isNaN(ticks[i])){ - return false; + LocusZoom.Panel.prototype.renderAxis = function (axis) { + if ([ + 'x', + 'y1', + 'y2' + ].indexOf(axis) === -1) { + throw 'Unable to render axis; invalid axis identifier: ' + axis; } - } - return true; - })(this[axis+"_ticks"]); - - // Initialize the axis; set scale and orientation - this[axis+"_axis"] = d3.svg.axis().scale(this[axis+"_scale"]).orient(axis_params[axis].orientation).tickPadding(3); - - // Set tick values and format - if (ticksAreAllNumbers){ - this[axis+"_axis"].tickValues(this[axis+"_ticks"]); - if (this.layout.axes[axis].tick_format === "region"){ - this[axis+"_axis"].tickFormat(function(d) { return LocusZoom.positionIntToString(d, 6); }); - } - } else { - var ticks = this[axis+"_ticks"].map(function(t){ - return(t[axis.substr(0,1)]); - }); - this[axis+"_axis"].tickValues(ticks) - .tickFormat(function(t, i) { return this[axis+"_ticks"][i].text; }.bind(this)); - } - - // Position the axis in the SVG and apply the axis construct - this.svg[axis+"_axis"] - .attr("transform", axis_params[axis].position) - .call(this[axis+"_axis"]); - - // If necessary manually apply styles and transforms to ticks as specified by the layout - if (!ticksAreAllNumbers){ - var tick_selector = d3.selectAll("g#" + this.getBaseId().replace(".","\\.") + "\\." + axis + "_axis g.tick"); - var panel = this; - tick_selector.each(function(d, i){ - var selector = d3.select(this).select("text"); - if (panel[axis+"_ticks"][i].style){ - selector.style(panel[axis+"_ticks"][i].style); - } - if (panel[axis+"_ticks"][i].transform){ - selector.attr("transform", panel[axis+"_ticks"][i].transform); + var canRender = this.layout.axes[axis].render && typeof this[axis + '_scale'] == 'function' && !isNaN(this[axis + '_scale'](0)); + // If the axis has already been rendered then check if we can/can't render it + // Make sure the axis element is shown/hidden to suit + if (this[axis + '_axis']) { + this.svg.container.select('g.lz-axis.lz-' + axis).style('display', canRender ? null : 'none'); } - }); - } - - // Render the axis label if necessary - var label = this.layout.axes[axis].label || null; - if (label !== null){ - this.svg[axis+"_axis_label"] - .attr("x", axis_params[axis].label_x).attr("y", axis_params[axis].label_y) - .text(LocusZoom.parseFields(this.state, label)); - if (axis_params[axis].label_rotate !== null){ - this.svg[axis+"_axis_label"] - .attr("transform", "rotate(" + axis_params[axis].label_rotate + " " + axis_params[axis].label_x + "," + axis_params[axis].label_y + ")"); - } - } - - // Attach interactive handlers to ticks as needed - ["x", "y1", "y2"].forEach(function(axis){ - if (this.layout.interaction["drag_" + axis + "_ticks_to_scale"]){ - var namespace = "." + this.parent.id + "." + this.id + ".interaction.drag"; - var tick_mouseover = function(){ - if (typeof d3.select(this).node().focus == "function"){ d3.select(this).node().focus(); } - var cursor = (axis === "x") ? "ew-resize" : "ns-resize"; - if (d3.event && d3.event.shiftKey){ cursor = "move"; } - d3.select(this) - .style({"font-weight": "bold", "cursor": cursor}) - .on("keydown" + namespace, tick_mouseover) - .on("keyup" + namespace, tick_mouseover); - }; - this.svg.container.selectAll(".lz-axis.lz-" + axis + " .tick text") - .attr("tabindex", 0) // necessary to make the tick focusable so keypress events can be captured - .on("mouseover" + namespace, tick_mouseover) - .on("mouseout" + namespace, function(){ - d3.select(this).style({"font-weight": "normal"}); - d3.select(this).on("keydown" + namespace, null).on("keyup" + namespace, null); - }) - .on("mousedown" + namespace, function(){ - this.parent.startDrag(this, axis + "_tick"); + if (!canRender) { + return this; + } + // Axis-specific values to plug in where needed + var axis_params = { + x: { + position: 'translate(' + this.layout.margin.left + ',' + (this.layout.height - this.layout.margin.bottom) + ')', + orientation: 'bottom', + label_x: this.layout.cliparea.width / 2, + label_y: this.layout.axes[axis].label_offset || 0, + label_rotate: null + }, + y1: { + position: 'translate(' + this.layout.margin.left + ',' + this.layout.margin.top + ')', + orientation: 'left', + label_x: -1 * (this.layout.axes[axis].label_offset || 0), + label_y: this.layout.cliparea.height / 2, + label_rotate: -90 + }, + y2: { + position: 'translate(' + (this.layout.width - this.layout.margin.right) + ',' + this.layout.margin.top + ')', + orientation: 'right', + label_x: this.layout.axes[axis].label_offset || 0, + label_y: this.layout.cliparea.height / 2, + label_rotate: -90 + } + }; + // Generate Ticks + this[axis + '_ticks'] = this.generateTicks(axis); + // Determine if the ticks are all numbers (d3-automated tick rendering) or not (manual tick rendering) + var ticksAreAllNumbers = function (ticks) { + for (var i = 0; i < ticks.length; i++) { + if (isNaN(ticks[i])) { + return false; + } + } + return true; + }(this[axis + '_ticks']); + // Initialize the axis; set scale and orientation + this[axis + '_axis'] = d3.svg.axis().scale(this[axis + '_scale']).orient(axis_params[axis].orientation).tickPadding(3); + // Set tick values and format + if (ticksAreAllNumbers) { + this[axis + '_axis'].tickValues(this[axis + '_ticks']); + if (this.layout.axes[axis].tick_format === 'region') { + this[axis + '_axis'].tickFormat(function (d) { + return LocusZoom.positionIntToString(d, 6); + }); + } + } else { + var ticks = this[axis + '_ticks'].map(function (t) { + return t[axis.substr(0, 1)]; + }); + this[axis + '_axis'].tickValues(ticks).tickFormat(function (t, i) { + return this[axis + '_ticks'][i].text; }.bind(this)); - } - }.bind(this)); - - return this; - -}; - -/** + } + // Position the axis in the SVG and apply the axis construct + this.svg[axis + '_axis'].attr('transform', axis_params[axis].position).call(this[axis + '_axis']); + // If necessary manually apply styles and transforms to ticks as specified by the layout + if (!ticksAreAllNumbers) { + var tick_selector = d3.selectAll('g#' + this.getBaseId().replace('.', '\\.') + '\\.' + axis + '_axis g.tick'); + var panel = this; + tick_selector.each(function (d, i) { + var selector = d3.select(this).select('text'); + if (panel[axis + '_ticks'][i].style) { + selector.style(panel[axis + '_ticks'][i].style); + } + if (panel[axis + '_ticks'][i].transform) { + selector.attr('transform', panel[axis + '_ticks'][i].transform); + } + }); + } + // Render the axis label if necessary + var label = this.layout.axes[axis].label || null; + if (label !== null) { + this.svg[axis + '_axis_label'].attr('x', axis_params[axis].label_x).attr('y', axis_params[axis].label_y).text(LocusZoom.parseFields(this.state, label)); + if (axis_params[axis].label_rotate !== null) { + this.svg[axis + '_axis_label'].attr('transform', 'rotate(' + axis_params[axis].label_rotate + ' ' + axis_params[axis].label_x + ',' + axis_params[axis].label_y + ')'); + } + } + // Attach interactive handlers to ticks as needed + [ + 'x', + 'y1', + 'y2' + ].forEach(function (axis) { + if (this.layout.interaction['drag_' + axis + '_ticks_to_scale']) { + var namespace = '.' + this.parent.id + '.' + this.id + '.interaction.drag'; + var tick_mouseover = function () { + if (typeof d3.select(this).node().focus == 'function') { + d3.select(this).node().focus(); + } + var cursor = axis === 'x' ? 'ew-resize' : 'ns-resize'; + if (d3.event && d3.event.shiftKey) { + cursor = 'move'; + } + d3.select(this).style({ + 'font-weight': 'bold', + 'cursor': cursor + }).on('keydown' + namespace, tick_mouseover).on('keyup' + namespace, tick_mouseover); + }; + this.svg.container.selectAll('.lz-axis.lz-' + axis + ' .tick text').attr('tabindex', 0) // necessary to make the tick focusable so keypress events can be captured +.on('mouseover' + namespace, tick_mouseover).on('mouseout' + namespace, function () { + d3.select(this).style({ 'font-weight': 'normal' }); + d3.select(this).on('keydown' + namespace, null).on('keyup' + namespace, null); + }).on('mousedown' + namespace, function () { + this.parent.startDrag(this, axis + '_tick'); + }.bind(this)); + } + }.bind(this)); + return this; + }; + /** * Force the height of this panel to the largest absolute height of the data in * all child data layers (if not null for any child data layers) * @param {number} [target_height] A target height, which will be used in situations when the expected height can be * pre-calculated (eg when the layers are transitioning) */ -LocusZoom.Panel.prototype.scaleHeightToData = function(target_height){ - target_height = +target_height || null; - if (target_height === null){ - this.data_layer_ids_by_z_index.forEach(function(id){ - var dh = this.data_layers[id].getAbsoluteDataHeight(); - if (+dh){ - if (target_height === null){ target_height = +dh; } - else { target_height = Math.max(target_height, +dh); } + LocusZoom.Panel.prototype.scaleHeightToData = function (target_height) { + target_height = +target_height || null; + if (target_height === null) { + this.data_layer_ids_by_z_index.forEach(function (id) { + var dh = this.data_layers[id].getAbsoluteDataHeight(); + if (+dh) { + if (target_height === null) { + target_height = +dh; + } else { + target_height = Math.max(target_height, +dh); + } + } + }.bind(this)); } - }.bind(this)); - } - if (+target_height){ - target_height += +this.layout.margin.top + +this.layout.margin.bottom; - this.setDimensions(this.layout.width, target_height); - this.parent.setDimensions(); - this.parent.panel_ids_by_y_index.forEach(function(id){ - this.parent.panels[id].layout.proportional_height = null; - }.bind(this)); - this.parent.positionPanels(); - } -}; - -/** + if (+target_height) { + target_height += +this.layout.margin.top + +this.layout.margin.bottom; + this.setDimensions(this.layout.width, target_height); + this.parent.setDimensions(); + this.parent.panel_ids_by_y_index.forEach(function (id) { + this.parent.panels[id].layout.proportional_height = null; + }.bind(this)); + this.parent.positionPanels(); + } + }; + /** * Methods to set/unset element statuses across all data layers * @param {String} status * @param {Boolean} toggle * @param {Array} filters * @param {Boolean} exclusive */ -LocusZoom.Panel.prototype.setElementStatusByFilters = function(status, toggle, filters, exclusive){ - this.data_layer_ids_by_z_index.forEach(function(id){ - this.data_layers[id].setElementStatusByFilters(status, toggle, filters, exclusive); - }.bind(this)); -}; -/** + LocusZoom.Panel.prototype.setElementStatusByFilters = function (status, toggle, filters, exclusive) { + this.data_layer_ids_by_z_index.forEach(function (id) { + this.data_layers[id].setElementStatusByFilters(status, toggle, filters, exclusive); + }.bind(this)); + }; + /** * Set/unset element statuses across all data layers * @param {String} status * @param {Boolean} toggle */ -LocusZoom.Panel.prototype.setAllElementStatus = function(status, toggle){ - this.data_layer_ids_by_z_index.forEach(function(id){ - this.data_layers[id].setAllElementStatus(status, toggle); - }.bind(this)); -}; -// TODO: Capture documentation for dynamically generated methods -LocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){ - var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx]; - var antiverb = "un" + verb; - // Set/unset status for arbitrarily many elements given a set of filters - LocusZoom.Panel.prototype[verb + "ElementsByFilters"] = function(filters, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - return this.setElementStatusByFilters(adjective, true, filters, exclusive); - }; - LocusZoom.Panel.prototype[antiverb + "ElementsByFilters"] = function(filters, exclusive){ - if (typeof exclusive == "undefined"){ exclusive = false; } else { exclusive = !!exclusive; } - return this.setElementStatusByFilters(adjective, false, filters, exclusive); - }; - // Set/unset status for all elements - LocusZoom.Panel.prototype[verb + "AllElements"] = function(){ - this.setAllElementStatus(adjective, true); - return this; - }; - LocusZoom.Panel.prototype[antiverb + "AllElements"] = function(){ - this.setAllElementStatus(adjective, false); - return this; - }; -}); - - -/** + LocusZoom.Panel.prototype.setAllElementStatus = function (status, toggle) { + this.data_layer_ids_by_z_index.forEach(function (id) { + this.data_layers[id].setAllElementStatus(status, toggle); + }.bind(this)); + }; + // TODO: Capture documentation for dynamically generated methods + LocusZoom.DataLayer.Statuses.verbs.forEach(function (verb, idx) { + var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx]; + var antiverb = 'un' + verb; + // Set/unset status for arbitrarily many elements given a set of filters + LocusZoom.Panel.prototype[verb + 'ElementsByFilters'] = function (filters, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + return this.setElementStatusByFilters(adjective, true, filters, exclusive); + }; + LocusZoom.Panel.prototype[antiverb + 'ElementsByFilters'] = function (filters, exclusive) { + if (typeof exclusive == 'undefined') { + exclusive = false; + } else { + exclusive = !!exclusive; + } + return this.setElementStatusByFilters(adjective, false, filters, exclusive); + }; + // Set/unset status for all elements + LocusZoom.Panel.prototype[verb + 'AllElements'] = function () { + this.setAllElementStatus(adjective, true); + return this; + }; + LocusZoom.Panel.prototype[antiverb + 'AllElements'] = function () { + this.setAllElementStatus(adjective, false); + return this; + }; + }); + /** * Add a "basic" loader to a panel * This method is just a shortcut for adding the most commonly used type of loading indicator, which appears when * data is requested, animates (e.g. shows an infinitely cycling progress bar as opposed to one that loads from @@ -10848,25 +10764,23 @@ LocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){ * @param {Boolean} show_immediately * @returns {LocusZoom.Panel} */ -LocusZoom.Panel.prototype.addBasicLoader = function(show_immediately){ - if (typeof show_immediately != "undefined"){ show_immediately = true; } - if (show_immediately){ - this.loader.show("Loading...").animate(); - } - this.on("data_requested", function(){ - this.loader.show("Loading...").animate(); - }.bind(this)); - this.on("data_rendered", function(){ - this.loader.hide(); - }.bind(this)); - return this; -}; - - - } catch (plugin_loading_error){ - console.error("LocusZoom Plugin error: " + plugin_loading_error); + LocusZoom.Panel.prototype.addBasicLoader = function (show_immediately) { + if (typeof show_immediately != 'undefined') { + show_immediately = true; + } + if (show_immediately) { + this.loader.show('Loading...').animate(); + } + this.on('data_requested', function () { + this.loader.show('Loading...').animate(); + }.bind(this)); + this.on('data_rendered', function () { + this.loader.hide(); + }.bind(this)); + return this; + }; + } catch (plugin_loading_error) { + console.error('LocusZoom Plugin error: ' + plugin_loading_error); } - return LocusZoom; - })); \ No newline at end of file diff --git a/dist/locuszoom.app.min.js b/dist/locuszoom.app.min.js index 5051f43c..53c3244b 100644 --- a/dist/locuszoom.app.min.js +++ b/dist/locuszoom.app.min.js @@ -1,6 +1,7 @@ -!function(t,e){"function"==typeof define&&define.amd?define(["postal"],function(a,i){return t.LocusZoom=e(a,i)}):"object"==typeof module&&module.exports?module.exports=t.LocusZoom=e(require("d3"),require("Q")):t.LocusZoom=e(t.d3,t.Q)}(this,function(t,e){var a=function(t,e){if(e==t)return!0;var a=t.split("."),i=e.split("."),n=!1;return a.forEach(function(t,e){!n&&+i[e]>+a[e]&&(n=!0)}),n};try{var i="3.5.6";if("object"!=typeof t)throw"d3 dependency not met. Library missing.";if(!a(i,t.version))throw"d3 dependency not met. Outdated version detected.\nRequired d3 version: "+i+" or higher (found: "+t.version+").";if("function"!=typeof e)throw"Q dependency not met. Library missing.";var n={version:"0.7.1"};n.populate=function(e,a,i){if("undefined"==typeof e)throw"LocusZoom.populate selector not defined";t.select(e).html("");var s;return t.select(e).call(function(){if("undefined"==typeof this.node().id){for(var e=0;!t.select("#lz-"+e).empty();)e++;this.attr("id","#lz-"+e)}if(s=new n.Plot(this.node().id,a,i),s.container=this.node(),"undefined"!=typeof this.node().dataset&&"undefined"!=typeof this.node().dataset.region){var o=n.parsePositionQuery(this.node().dataset.region);Object.keys(o).forEach(function(t){s.state[t]=o[t]})}s.svg=t.select("div#"+s.id).append("svg").attr("version","1.1").attr("xmlns","http://www.w3.org/2000/svg").attr("id",s.id+"_svg").attr("class","lz-locuszoom").style(s.layout.style),s.setDimensions(),s.positionPanels(),s.initialize(),"object"==typeof a&&Object.keys(a).length&&s.refresh()}),s},n.populateAll=function(e,a,i){var s=[];return t.selectAll(e).each(function(t,e){s[e]=n.populate(this,a,i)}),s},n.positionIntToString=function(t,e,a){var i={0:"",3:"K",6:"M",9:"G"};if(a=a||!1,isNaN(e)||null===e){var n=Math.log(t)/Math.LN10;e=Math.min(Math.max(n-n%3,0),9)}var s=e-Math.floor((Math.log(t)/Math.LN10).toFixed(e+3)),o=Math.min(Math.max(e,0),2),r=Math.min(Math.max(s,o),12),l=""+(t/Math.pow(10,e)).toFixed(r);return a&&"undefined"!=typeof i[e]&&(l+=" "+i[e]+"b"),l},n.positionStringToInt=function(t){var e=t.toUpperCase();e=e.replace(/,/g,"");var a=/([KMG])[B]*$/,i=a.exec(e),n=1;return i&&(n="M"===i[1]?1e6:"G"===i[1]?1e9:1e3,e=e.replace(a,"")),e=Number(e)*n},n.parsePositionQuery=function(t){var e=/^(\w+):([\d,.]+[kmgbKMGB]*)([-+])([\d,.]+[kmgbKMGB]*)$/,a=/^(\w+):([\d,.]+[kmgbKMGB]*)$/,i=e.exec(t);if(i){if("+"===i[3]){var s=n.positionStringToInt(i[2]),o=n.positionStringToInt(i[4]);return{chr:i[1],start:s-o,end:s+o}}return{chr:i[1],start:n.positionStringToInt(i[2]),end:n.positionStringToInt(i[4])}}return i=a.exec(t),i?{chr:i[1],position:n.positionStringToInt(i[2])}:null},n.prettyTicks=function(t,e,a){("undefined"==typeof a||isNaN(parseInt(a)))&&(a=5),a=parseInt(a);var i=a/3,n=.75,s=1.5,o=.5+1.5*s,r=Math.abs(t[0]-t[1]),l=r/a;Math.log(r)/Math.LN10<-2&&(l=Math.max(Math.abs(r))*n/i);var h=Math.pow(10,Math.floor(Math.log(l)/Math.LN10)),d=0;h<1&&0!==h&&(d=Math.abs(Math.round(Math.log(h)/Math.LN10)));var u=h;2*h-l0&&(c=parseFloat(c.toFixed(d)));return p.push(c),"undefined"!=typeof e&&["low","high","both","neither"].indexOf(e)!==-1||(e="neither"),"low"!==e&&"both"!==e||p[0]t[1]&&p.pop(),p},n.createCORSPromise=function(t,a,i,n,s){var o=e.defer(),r=new XMLHttpRequest;if("withCredentials"in r?r.open(t,a,!0):"undefined"!=typeof XDomainRequest?(r=new XDomainRequest,r.open(t,a)):r=null,r){if(r.onreadystatechange=function(){4===r.readyState&&(200===r.status||0===r.status?o.resolve(r.response):o.reject("HTTP "+r.status+" for "+a))},s&&setTimeout(o.reject,s),i="undefined"!=typeof i?i:"","undefined"!=typeof n)for(var l in n)r.setRequestHeader(l,n[l]);r.send(i)}return o.promise},n.validateState=function(t,e){t=t||{},e=e||{};var a=!1;if("undefined"!=typeof t.chr&&"undefined"!=typeof t.start&&"undefined"!=typeof t.end){var i,n=null;if(t.start=Math.max(parseInt(t.start),1),t.end=Math.max(parseInt(t.end),1),isNaN(t.start)&&isNaN(t.end))t.start=1,t.end=1,n=.5,i=0;else if(isNaN(t.start)||isNaN(t.end))n=t.start||t.end,i=0,t.start=isNaN(t.start)?t.end:t.start,t.end=isNaN(t.end)?t.start:t.end;else{if(n=Math.round((t.start+t.end)/2),i=t.end-t.start,i<0){var s=t.start;t.end=t.start,t.start=s,i=t.end-t.start}n<0&&(t.start=1,t.end=1,i=0)}a=!0}return!isNaN(e.min_region_scale)&&a&&ie.max_region_scale&&(t.start=Math.max(n-Math.floor(e.max_region_scale/2),1),t.end=t.start+e.max_region_scale),t},n.parseFields=function(t,e){if("object"!=typeof t)throw"LocusZoom.parseFields invalid arguments: data is not an object";if("string"!=typeof e)throw"LocusZoom.parseFields invalid arguments: html is not a string";for(var a=[],i=/\{\{(?:(#if )?([A-Za-z0-9_:|]+)|(\/if))\}\}/;e.length>0;){var s=i.exec(e);s?0!==s.index?(a.push({text:e.slice(0,s.index)}),e=e.slice(s.index)):"#if "===s[1]?(a.push({condition:s[2]}),e=e.slice(s[0].length)):s[2]?(a.push({variable:s[2]}),e=e.slice(s[0].length)):"/if"===s[3]?(a.push({close:"if"}),e=e.slice(s[0].length)):(console.error("Error tokenizing tooltip when remaining template is "+JSON.stringify(e)+" and previous tokens are "+JSON.stringify(a)+" and current regex match is "+JSON.stringify([s[1],s[2],s[3]])),e=e.slice(s[0].length)):(a.push({text:e}),e="")}for(var o=function(){var t=a.shift();if("undefined"!=typeof t.text||t.variable)return t;if(t.condition){for(t.then=[];a.length>0;){if("if"===a[0].close){a.shift();break}t.then.push(o())}return t}return console.error("Error making tooltip AST due to unknown token "+JSON.stringify(t)),{text:""}},r=[];a.length>0;)r.push(o());var l=function(e){return l.cache.hasOwnProperty(e)||(l.cache[e]=new n.Data.Field(e).resolve(t)),l.cache[e]};l.cache={};var h=function(t){if("undefined"!=typeof t.text)return t.text;if(t.variable){try{var e=l(t.variable);if(["string","number","boolean"].indexOf(typeof e)!==-1)return e;if(null===e)return""}catch(e){console.error("Error while processing variable "+JSON.stringify(t.variable))}return"{{"+t.variable+"}}"}if(t.condition){try{var a=l(t.condition);if(a||0===a)return t.then.map(h).join("")}catch(e){console.error("Error while processing condition "+JSON.stringify(t.variable))}return""}console.error("Error rendering tooltip due to unknown AST node "+JSON.stringify(t))};return r.map(h).join("")},n.getToolTipData=function(e){if("object"!=typeof e||"undefined"==typeof e.parentNode)throw"Invalid node object";var a=t.select(e);return a.classed("lz-data_layer-tooltip")&&"undefined"!=typeof a.data()[0]?a.data()[0]:n.getToolTipData(e.parentNode)},n.getToolTipDataLayer=function(t){var e=n.getToolTipData(t);return e.getDataLayer?e.getDataLayer():null},n.getToolTipPanel=function(t){var e=n.getToolTipDataLayer(t);return e?e.parent:null},n.getToolTipPlot=function(t){var e=n.getToolTipPanel(t);return e?e.parent:null},n.generateCurtain=function(){var e={showing:!1,selector:null,content_selector:null,hide_delay:null,show:function(e,a){return this.curtain.showing||(this.curtain.selector=t.select(this.parent_plot.svg.node().parentNode).insert("div").attr("class","lz-curtain").attr("id",this.id+".curtain"),this.curtain.content_selector=this.curtain.selector.append("div").attr("class","lz-curtain-content"),this.curtain.selector.append("div").attr("class","lz-curtain-dismiss").html("Dismiss").on("click",function(){this.curtain.hide()}.bind(this)),this.curtain.showing=!0),this.curtain.update(e,a)}.bind(this),update:function(t,e){if(!this.curtain.showing)return this.curtain;clearTimeout(this.curtain.hide_delay),"object"==typeof e&&this.curtain.selector.style(e);var a=this.getPageOrigin();return this.curtain.selector.style({top:a.y+"px",left:a.x+"px",width:this.layout.width+"px",height:this.layout.height+"px"}),this.curtain.content_selector.style({"max-width":this.layout.width-40+"px","max-height":this.layout.height-40+"px"}),"string"==typeof t&&this.curtain.content_selector.html(t),this.curtain}.bind(this),hide:function(t){return this.curtain.showing?"number"==typeof t?(clearTimeout(this.curtain.hide_delay),this.curtain.hide_delay=setTimeout(this.curtain.hide,t),this.curtain):(this.curtain.selector.remove(),this.curtain.selector=null,this.curtain.content_selector=null,this.curtain.showing=!1,this.curtain):this.curtain}.bind(this)};return e},n.generateLoader=function(){var e={showing:!1,selector:null,content_selector:null,progress_selector:null,cancel_selector:null,show:function(e){return this.loader.showing||(this.loader.selector=t.select(this.parent_plot.svg.node().parentNode).insert("div").attr("class","lz-loader").attr("id",this.id+".loader"),this.loader.content_selector=this.loader.selector.append("div").attr("class","lz-loader-content"),this.loader.progress_selector=this.loader.selector.append("div").attr("class","lz-loader-progress-container").append("div").attr("class","lz-loader-progress"),this.loader.showing=!0,"undefined"==typeof e&&(e="Loading...")),this.loader.update(e)}.bind(this),update:function(t,e){if(!this.loader.showing)return this.loader;clearTimeout(this.loader.hide_delay),"string"==typeof t&&this.loader.content_selector.html(t);var a=6,i=this.getPageOrigin(),n=this.loader.selector.node().getBoundingClientRect();return this.loader.selector.style({top:i.y+this.layout.height-n.height-a+"px",left:i.x+a+"px"}),"number"==typeof e&&this.loader.progress_selector.style({width:Math.min(Math.max(e,1),100)+"%"}),this.loader}.bind(this),animate:function(){return this.loader.progress_selector.classed("lz-loader-progress-animated",!0),this.loader}.bind(this),setPercentCompleted:function(t){return this.loader.progress_selector.classed("lz-loader-progress-animated",!1),this.loader.update(null,t)}.bind(this),hide:function(t){return this.loader.showing?"number"==typeof t?(clearTimeout(this.loader.hide_delay),this.loader.hide_delay=setTimeout(this.loader.hide,t),this.loader):(this.loader.selector.remove(),this.loader.selector=null,this.loader.content_selector=null,this.loader.progress_selector=null,this.loader.cancel_selector=null,this.loader.showing=!1,this.loader):this.loader}.bind(this)};return e},n.subclass=function(t,e,a){if("function"!=typeof t)throw"Parent must be a callable constructor";e=e||{};var i=a||function(){t.apply(this,arguments)};return i.prototype=Object.create(t.prototype),Object.keys(e).forEach(function(t){i.prototype[t]=e[t]}),i.prototype.constructor=i,i},n.Layouts=function(){var t={},e={plot:{},panel:{},data_layer:{},dashboard:{},tooltip:{}};return t.get=function(t,a,i){if("string"!=typeof t||"string"!=typeof a)throw"invalid arguments passed to LocusZoom.Layouts.get, requires string (layout type) and string (layout name)";if(e[t][a]){var s=n.Layouts.merge(i||{},e[t][a]);if(s.unnamespaced)return delete s.unnamespaced,JSON.parse(JSON.stringify(s));var o="";"string"==typeof s.namespace?o=s.namespace:"object"==typeof s.namespace&&Object.keys(s.namespace).length&&(o="undefined"!=typeof s.namespace.default?s.namespace.default:s.namespace[Object.keys(s.namespace)[0]].toString()),o+=o.length?":":"";var r=function(t,e){if(e?"string"==typeof e&&(e={default:e}):e={default:""},"string"==typeof t){for(var a,i,s,l,h=/\{\{namespace(\[[A-Za-z_0-9]+\]|)\}\}/g,d=[];null!==(a=h.exec(t));)i=a[0],s=a[1].length?a[1].replace(/(\[|\])/g,""):null,l=o,null!=e&&"object"==typeof e&&"undefined"!=typeof e[s]&&(l=e[s]+(e[s].length?":":"")),d.push({base:i,namespace:l});for(var u in d)t=t.replace(d[u].base,d[u].namespace)}else if("object"==typeof t&&null!=t){if("undefined"!=typeof t.namespace){var p="string"==typeof t.namespace?{default:t.namespace}:t.namespace;e=n.Layouts.merge(e,p)}var c,y;for(var f in t)"namespace"!==f&&(c=r(t[f],e),y=r(f,e),f!==y&&delete t[f],t[y]=c)}return t};return s=r(s,s.namespace),JSON.parse(JSON.stringify(s))}throw"layout type ["+t+"] name ["+a+"] not found"},t.set=function(t,a,i){if("string"!=typeof t||"string"!=typeof a||"object"!=typeof i)throw"unable to set new layout; bad arguments passed to set()";return e[t]||(e[t]={}),i?e[t][a]=JSON.parse(JSON.stringify(i)):(delete e[t][a],null)},t.add=function(e,a,i){return t.set(e,a,i)},t.list=function(t){if(e[t])return Object.keys(e[t]);var a={};return Object.keys(e).forEach(function(t){a[t]=Object.keys(e[t])}),a},t.merge=function(t,e){if("object"!=typeof t||"object"!=typeof e)throw"LocusZoom.Layouts.merge only accepts two layout objects; "+typeof t+", "+typeof e+" given";for(var a in e)if(e.hasOwnProperty(a)){var i=null===t[a]?"undefined":typeof t[a],s=typeof e[a];if("object"===i&&Array.isArray(t[a])&&(i="array"),"object"===s&&Array.isArray(e[a])&&(s="array"),"function"===i||"function"===s)throw"LocusZoom.Layouts.merge encountered an unsupported property type";"undefined"!==i?"object"!==i||"object"!==s||(t[a]=n.Layouts.merge(t[a],e[a])):t[a]=JSON.parse(JSON.stringify(e[a]))}return t},t}(),n.Layouts.add("tooltip","standard_association",{namespace:{assoc:"assoc"},closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:'{{{{namespace[assoc]}}variant}}
P Value: {{{{namespace[assoc]}}log_pvalue|logtoscinotation}}
Ref. Allele: {{{{namespace[assoc]}}ref_allele}}
Make LD Reference
'});var s=n.Layouts.get("tooltip","standard_association",{unnamespaced:!0});s.html+='Condition on Variant
',n.Layouts.add("tooltip","covariates_model_association",s),n.Layouts.add("tooltip","standard_genes",{closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:'

{{gene_name}}

Gene ID: {{gene_id}}
Transcript ID: {{transcript_id}}
ConstraintExpected variantsObserved variantsConst. Metric
Synonymous{{exp_syn}}{{n_syn}}z = {{syn_z}}
Missense{{exp_mis}}{{n_mis}}z = {{mis_z}}
LoF{{exp_lof}}{{n_lof}}pLI = {{pLI}}
More data on ExAC'}),n.Layouts.add("tooltip","standard_intervals",{namespace:{intervals:"intervals"},closable:!1,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:"{{{{namespace[intervals]}}state_name}}
{{{{namespace[intervals]}}start}}-{{{{namespace[intervals]}}end}}"}),n.Layouts.add("data_layer","significance",{id:"significance",type:"orthogonal_line",orientation:"horizontal",offset:4.522}),n.Layouts.add("data_layer","recomb_rate",{namespace:{recomb:"recomb"},id:"recombrate",type:"line",fields:["{{namespace[recomb]}}position","{{namespace[recomb]}}recomb_rate"],z_index:1,style:{stroke:"#0000FF","stroke-width":"1.5px"},x_axis:{field:"{{namespace[recomb]}}position"},y_axis:{axis:2,field:"{{namespace[recomb]}}recomb_rate",floor:0,ceiling:100}}),n.Layouts.add("data_layer","association_pvalues",{namespace:{assoc:"assoc",ld:"ld"},id:"associationpvalues",type:"scatter",point_shape:{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:"diamond",else:"circle"}},point_size:{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:80,else:40}},color:[{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:"#9632b8"}},{scale_function:"numerical_bin",field:"{{namespace[ld]}}state",parameters:{breaks:[0,.2,.4,.6,.8],values:["#357ebd","#46b8da","#5cb85c","#eea236","#d43f3a"]}},"#B8B8B8"],legend:[{shape:"diamond",color:"#9632b8",size:40,label:"LD Ref Var",class:"lz-data_layer-scatter"},{shape:"circle",color:"#d43f3a",size:40,label:"1.0 > r² ≥ 0.8",class:"lz-data_layer-scatter"},{shape:"circle",color:"#eea236",size:40,label:"0.8 > r² ≥ 0.6",class:"lz-data_layer-scatter"},{shape:"circle",color:"#5cb85c",size:40,label:"0.6 > r² ≥ 0.4",class:"lz-data_layer-scatter"},{shape:"circle",color:"#46b8da",size:40,label:"0.4 > r² ≥ 0.2",class:"lz-data_layer-scatter"},{shape:"circle",color:"#357ebd",size:40,label:"0.2 > r² ≥ 0.0",class:"lz-data_layer-scatter"},{shape:"circle",color:"#B8B8B8",size:40,label:"no r² data",class:"lz-data_layer-scatter"}],fields:["{{namespace[assoc]}}variant","{{namespace[assoc]}}position","{{namespace[assoc]}}log_pvalue","{{namespace[assoc]}}log_pvalue|logtoscinotation","{{namespace[assoc]}}ref_allele","{{namespace[ld]}}state","{{namespace[ld]}}isrefvar"],id_field:"{{namespace[assoc]}}variant",z_index:2,x_axis:{field:"{{namespace[assoc]}}position"},y_axis:{axis:1,field:"{{namespace[assoc]}}log_pvalue",floor:0,upper_buffer:.1,min_extent:[0,10]},behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_association",{unnamespaced:!0})}),n.Layouts.add("data_layer","phewas_pvalues",{namespace:{phewas:"phewas"},id:"phewaspvalues",type:"category_scatter",point_shape:"circle",point_size:70,tooltip_positioning:"vertical",id_field:"{{namespace[phewas]}}id",fields:["{{namespace[phewas]}}id","{{namespace[phewas]}}log_pvalue","{{namespace[phewas]}}trait_group","{{namespace[phewas]}}trait_label"],x_axis:{field:"{{namespace[phewas]}}x",category_field:"{{namespace[phewas]}}trait_group"},y_axis:{axis:1,field:"{{namespace[phewas]}}log_pvalue",floor:0,upper_buffer:.15},color:{field:"{{namespace[phewas]}}trait_group",scale_function:"categorical_bin",parameters:{categories:[],values:[],null_value:"#B8B8B8"}},fill_opacity:.7,tooltip:{closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:["Trait: {{{{namespace[phewas]}}trait_label|htmlescape}}
","Trait Category: {{{{namespace[phewas]}}trait_group|htmlescape}}
","P-value: {{{{namespace[phewas]}}log_pvalue|logtoscinotation|htmlescape}}
"].join("")},behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},label:{text:"{{{{namespace[phewas]}}trait_label}}",spacing:6,lines:{style:{"stroke-width":"2px",stroke:"#333333","stroke-dasharray":"2px 2px"}},filters:[{field:"{{namespace[phewas]}}log_pvalue",operator:">=",value:20}],style:{"font-size":"14px","font-weight":"bold",fill:"#333333"}}}),n.Layouts.add("data_layer","genes",{namespace:{gene:"gene",constraint:"constraint"},id:"genes",type:"genes",fields:["{{namespace[gene]}}gene","{{namespace[constraint]}}constraint"],id_field:"gene_id",behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_genes",{unnamespaced:!0})}),n.Layouts.add("data_layer","genome_legend",{namespace:{genome:"genome"},id:"genome_legend",type:"genome_legend",fields:["{{namespace[genome]}}chr","{{namespace[genome]}}base_pairs"],x_axis:{floor:0,ceiling:2881033286}}),n.Layouts.add("data_layer","intervals",{namespace:{intervals:"intervals"},id:"intervals",type:"intervals",fields:["{{namespace[intervals]}}start","{{namespace[intervals]}}end","{{namespace[intervals]}}state_id","{{namespace[intervals]}}state_name"],id_field:"{{namespace[intervals]}}start",start_field:"{{namespace[intervals]}}start",end_field:"{{namespace[intervals]}}end",track_split_field:"{{namespace[intervals]}}state_id",split_tracks:!0,always_hide_legend:!1,color:{field:"{{namespace[intervals]}}state_id",scale_function:"categorical_bin",parameters:{categories:[1,2,3,4,5,6,7,8,9,10,11,12,13],values:["rgb(212,63,58)","rgb(250,120,105)","rgb(252,168,139)","rgb(240,189,66)","rgb(250,224,105)","rgb(240,238,84)","rgb(244,252,23)","rgb(23,232,252)","rgb(32,191,17)","rgb(23,166,77)","rgb(32,191,17)","rgb(162,133,166)","rgb(212,212,212)"],null_value:"#B8B8B8"}},legend:[{shape:"rect",color:"rgb(212,63,58)",width:9,label:"Active Promoter","{{namespace[intervals]}}state_id":1},{shape:"rect",color:"rgb(250,120,105)",width:9,label:"Weak Promoter","{{namespace[intervals]}}state_id":2},{shape:"rect",color:"rgb(252,168,139)",width:9,label:"Poised Promoter","{{namespace[intervals]}}state_id":3},{shape:"rect",color:"rgb(240,189,66)",width:9,label:"Strong enhancer","{{namespace[intervals]}}state_id":4},{shape:"rect",color:"rgb(250,224,105)",width:9,label:"Strong enhancer","{{namespace[intervals]}}state_id":5},{shape:"rect",color:"rgb(240,238,84)",width:9,label:"Weak enhancer","{{namespace[intervals]}}state_id":6},{shape:"rect",color:"rgb(244,252,23)",width:9,label:"Weak enhancer","{{namespace[intervals]}}state_id":7},{shape:"rect",color:"rgb(23,232,252)",width:9,label:"Insulator","{{namespace[intervals]}}state_id":8},{shape:"rect",color:"rgb(32,191,17)",width:9,label:"Transcriptional transition","{{namespace[intervals]}}state_id":9},{shape:"rect",color:"rgb(23,166,77)",width:9,label:"Transcriptional elongation","{{namespace[intervals]}}state_id":10},{shape:"rect",color:"rgb(136,240,129)",width:9,label:"Weak transcribed","{{namespace[intervals]}}state_id":11},{shape:"rect",color:"rgb(162,133,166)",width:9,label:"Polycomb-repressed","{{namespace[intervals]}}state_id":12},{shape:"rect",color:"rgb(212,212,212)",width:9,label:"Heterochromatin / low signal","{{namespace[intervals]}}state_id":13}],behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_intervals",{unnamespaced:!0})}),n.Layouts.add("dashboard","standard_panel",{components:[{type:"remove_panel",position:"right",color:"red",group_position:"end"},{type:"move_panel_up",position:"right",group_position:"middle"},{type:"move_panel_down",position:"right",group_position:"start",style:{"margin-left":"0.75em"}}]}),n.Layouts.add("dashboard","standard_plot",{components:[{type:"title",title:"LocusZoom",subtitle:'v'+n.version+"",position:"left"},{type:"dimensions",position:"right"},{type:"region_scale",position:"right"},{type:"download",position:"right"}]});var o=n.Layouts.get("dashboard","standard_plot");o.components.push({type:"covariates_model",button_html:"Model",button_title:"Show and edit covariates currently in model",position:"left"}),n.Layouts.add("dashboard","covariates_model_plot",o);var r=n.Layouts.get("dashboard","standard_plot");r.components.push({type:"shift_region",step:5e5,button_html:">>",position:"right",group_position:"end"}),r.components.push({type:"shift_region",step:5e4,button_html:">",position:"right",group_position:"middle"}),r.components.push({type:"zoom_region",step:.2,position:"right",group_position:"middle"}),r.components.push({type:"zoom_region",step:-.2,position:"right",group_position:"middle"}),r.components.push({type:"shift_region",step:-5e4,button_html:"<",position:"right",group_position:"middle"}),r.components.push({type:"shift_region",step:-5e5,button_html:"<<",position:"right",group_position:"start"}),n.Layouts.add("dashboard","region_nav_plot",r),n.Layouts.add("panel","association",{id:"association",width:800,height:225,min_width:400,min_height:200,proportional_width:1,margin:{top:35,right:50,bottom:40,left:50},inner_border:"rgb(210, 210, 210)",dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"toggle_legend",position:"right"}),t}(),axes:{x:{label:"Chromosome {{chr}} (Mb)",label_offset:32,tick_format:"region",extent:"state"},y1:{label:"-log10 p-value",label_offset:28},y2:{label:"Recombination Rate (cM/Mb)",label_offset:40}},legend:{orientation:"vertical",origin:{x:55,y:40},hidden:!0},interaction:{drag_background_to_pan:!0,drag_x_ticks_to_scale:!0,drag_y1_ticks_to_scale:!0,drag_y2_ticks_to_scale:!0,scroll_to_zoom:!0,x_linked:!0},data_layers:[n.Layouts.get("data_layer","significance",{unnamespaced:!0}),n.Layouts.get("data_layer","recomb_rate",{unnamespaced:!0}),n.Layouts.get("data_layer","association_pvalues",{unnamespaced:!0})]}),n.Layouts.add("panel","genes",{id:"genes",width:800,height:225,min_width:400,min_height:112.5,proportional_width:1,margin:{top:20,right:50,bottom:20,left:50},axes:{},interaction:{drag_background_to_pan:!0,scroll_to_zoom:!0,x_linked:!0},dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"resize_to_data",position:"right"}),t}(),data_layers:[n.Layouts.get("data_layer","genes",{unnamespaced:!0})]}),n.Layouts.add("panel","phewas",{id:"phewas",width:800,height:300,min_width:800,min_height:300,proportional_width:1,margin:{top:20,right:50,bottom:120,left:50},inner_border:"rgb(210, 210, 210)",axes:{x:{ticks:{style:{"font-weight":"bold","font-size":"11px","text-anchor":"start"},transform:"rotate(50)",position:"left"}},y1:{label:"-log10 p-value",label_offset:28}},data_layers:[n.Layouts.get("data_layer","significance",{unnamespaced:!0}),n.Layouts.get("data_layer","phewas_pvalues",{unnamespaced:!0})]}),n.Layouts.add("panel","genome_legend",{id:"genome_legend",width:800,height:50,origin:{x:0,y:300},min_width:800,min_height:50,proportional_width:1,margin:{top:0,right:50,bottom:35,left:50},axes:{x:{label:"Genomic Position (number denotes chromosome)",label_offset:35,ticks:[{x:124625310,text:"1",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:370850307,text:"2",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:591461209,text:"3",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:786049562,text:"4",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:972084330,text:"5",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1148099493,text:"6",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1313226358,text:"7",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1465977701,text:"8",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1609766427,text:"9",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1748140516,text:"10",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1883411148,text:"11",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2017840353,text:"12",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2142351240,text:"13",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2253610949,text:"14",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2358551415,text:"15",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2454994487,text:"16",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2540769469,text:"17",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2620405698,text:"18",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2689008813,text:"19",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2750086065,text:"20",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2805663772,text:"21",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2855381003,text:"22",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"}]}},data_layers:[n.Layouts.get("data_layer","genome_legend",{unnamespaced:!0})]}),n.Layouts.add("panel","intervals",{id:"intervals",width:1e3,height:50,min_width:500,min_height:50,margin:{top:25,right:150,bottom:5,left:50},dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"toggle_split_tracks",data_layer_id:"intervals",position:"right"}),t}(),axes:{},interaction:{drag_background_to_pan:!0,scroll_to_zoom:!0,x_linked:!0},legend:{hidden:!0,orientation:"horizontal",origin:{x:50,y:0},pad_from_bottom:5},data_layers:[n.Layouts.get("data_layer","intervals",{unnamespaced:!0})]}),n.Layouts.add("plot","standard_association",{state:{},width:800,height:450,responsive_resize:!0,min_region_scale:2e4,max_region_scale:1e6,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","association",{unnamespaced:!0,proportional_height:.5}),n.Layouts.get("panel","genes",{unnamespaced:!0,proportional_height:.5})]}),n.StandardLayout=n.Layouts.get("plot","standard_association"),n.Layouts.add("plot","standard_phewas",{width:800,height:600,min_width:800,min_height:600,responsive_resize:!0,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","phewas",{unnamespaced:!0,proportional_height:.45}),n.Layouts.get("panel","genome_legend",{unnamespaced:!0,proportional_height:.1}),n.Layouts.get("panel","genes",{unnamespaced:!0,proportional_height:.45,margin:{bottom:40},axes:{x:{label:"Chromosome {{chr}} (Mb)",label_offset:32,tick_format:"region",extent:"state"}}})],mouse_guide:!1}),n.Layouts.add("plot","interval_association",{state:{},width:800,height:550,responsive_resize:!0,min_region_scale:2e4,max_region_scale:1e6,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","association",{unnamespaced:!0,width:800,proportional_height:225/570}),n.Layouts.get("panel","intervals",{unnamespaced:!0,proportional_height:120/570}),n.Layouts.get("panel","genes",{unnamespaced:!0,width:800,proportional_height:225/570})]}),n.DataLayer=function(t,e){return this.initialized=!1,this.layout_idx=null,this.id=null,this.parent=e||null,this.svg={},this.parent_plot=null,"undefined"!=typeof e&&e instanceof n.Panel&&(this.parent_plot=e.parent),this.layout=n.Layouts.merge(t||{},n.DataLayer.DefaultLayout),this.layout.id&&(this.id=this.layout.id),this.layout.x_axis!=={}&&"number"!=typeof this.layout.x_axis.axis&&(this.layout.x_axis.axis=1),this.layout.y_axis!=={}&&"number"!=typeof this.layout.y_axis.axis&&(this.layout.y_axis.axis=1),this.state={},this.state_id=null,this.setDefaultState(),this.data=[], -this.layout.tooltip&&(this.tooltips={}),this.global_statuses={highlighted:!1,selected:!1,faded:!1,hidden:!1},this},n.DataLayer.prototype.addField=function(t,e,a){if(!t||!e)throw"Must specify field name and namespace to use when adding field";var i=e+":"+t;if(a)if(i+="|","string"==typeof a)i+=a;else{if(!Array.isArray(a))throw"Must provide transformations as either a string or array of strings";i+=a.join("|")}var n=this.layout.fields;return n.indexOf(i)===-1&&n.push(i),i},n.DataLayer.prototype.setDefaultState=function(){this.parent&&(this.state=this.parent.state,this.state_id=this.parent.id+"."+this.id,this.state[this.state_id]=this.state[this.state_id]||{},n.DataLayer.Statuses.adjectives.forEach(function(t){this.state[this.state_id][t]=this.state[this.state_id][t]||[]}.bind(this)))},n.DataLayer.DefaultLayout={type:"",fields:[],x_axis:{},y_axis:{}},n.DataLayer.Statuses={verbs:["highlight","select","fade","hide"],adjectives:["highlighted","selected","faded","hidden"],menu_antiverbs:["unhighlight","deselect","unfade","show"]},n.DataLayer.prototype.getBaseId=function(){return this.parent_plot.id+"."+this.parent.id+"."+this.id},n.DataLayer.prototype.getAbsoluteDataHeight=function(){var t=this.svg.group.node().getBoundingClientRect();return t.height},n.DataLayer.prototype.canTransition=function(){return!!this.layout.transition&&!(this.parent_plot.panel_boundaries.dragging||this.parent_plot.interaction.panel_id)},n.DataLayer.prototype.getElementId=function(t){var e="element";if("string"==typeof t)e=t;else if("object"==typeof t){var a=this.layout.id_field||"id";if("undefined"==typeof t[a])throw"Unable to generate element ID";e=t[a].toString().replace(/\W/g,"")}return(this.getBaseId()+"-"+e).replace(/(:|\.|\[|\]|,)/g,"_")},n.DataLayer.prototype.getElementStatusNodeId=function(t){return null},n.DataLayer.prototype.getElementById=function(e){var a=t.select("#"+e.replace(/(:|\.|\[|\]|,)/g,"\\$1"));return!a.empty()&&a.data()&&a.data().length?a.data()[0]:null},n.DataLayer.prototype.applyDataMethods=function(){return this.data.forEach(function(t,e){this.data[e].toHTML=function(){var t=this.layout.id_field||"id",a="";return this.data[e][t]&&(a=this.data[e][t].toString()),a}.bind(this),this.data[e].getDataLayer=function(){return this}.bind(this),this.data[e].deselect=function(){var t=this.getDataLayer();t.unselectElement(this)}}.bind(this)),this.applyCustomDataMethods(),this},n.DataLayer.prototype.applyCustomDataMethods=function(){return this},n.DataLayer.prototype.initialize=function(){return this.svg.container=this.parent.svg.group.append("g").attr("class","lz-data_layer-container").attr("id",this.getBaseId()+".data_layer_container"),this.svg.clipRect=this.svg.container.append("clipPath").attr("id",this.getBaseId()+".clip").append("rect"),this.svg.group=this.svg.container.append("g").attr("id",this.getBaseId()+".data_layer").attr("clip-path","url(#"+this.getBaseId()+".clip)"),this},n.DataLayer.prototype.moveUp=function(){return this.parent.data_layer_ids_by_z_index[this.layout.z_index+1]&&(this.parent.data_layer_ids_by_z_index[this.layout.z_index]=this.parent.data_layer_ids_by_z_index[this.layout.z_index+1],this.parent.data_layer_ids_by_z_index[this.layout.z_index+1]=this.id,this.parent.resortDataLayers()),this},n.DataLayer.prototype.moveDown=function(){return this.parent.data_layer_ids_by_z_index[this.layout.z_index-1]&&(this.parent.data_layer_ids_by_z_index[this.layout.z_index]=this.parent.data_layer_ids_by_z_index[this.layout.z_index-1],this.parent.data_layer_ids_by_z_index[this.layout.z_index-1]=this.id,this.parent.resortDataLayers()),this},n.DataLayer.prototype.resolveScalableParameter=function(t,e){var a=null;if(Array.isArray(t))for(var i=0;null===a&&i":function(t,e){return t>e},">=":function(t,e){return t>=e},"%":function(t,e){return t%e}};return!!Array.isArray(e)&&(2===e.length?t[e[0]]===e[1]:!(3!==e.length||!a[e[1]])&&a[e[1]](t[e[0]],e[2]))},i=[];return this.data.forEach(function(n,s){var o=!0;t.forEach(function(t){a(n,t)||(o=!1)}),o&&i.push("indexes"===e?s:n)}),i},n.DataLayer.prototype.filterIndexes=function(t){return this.filter(t,"indexes")},n.DataLayer.prototype.filterElements=function(t){return this.filter(t,"elements")},n.DataLayer.Statuses.verbs.forEach(function(t,e){var a=n.DataLayer.Statuses.adjectives[e],i="un"+t;n.DataLayer.prototype[t+"Element"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatus(a,t,!0,e),this},n.DataLayer.prototype[i+"Element"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatus(a,t,!1,e),this},n.DataLayer.prototype[t+"ElementsByFilters"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatusByFilters(a,!0,t,e)},n.DataLayer.prototype[i+"ElementsByFilters"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatusByFilters(a,!1,t,e)},n.DataLayer.prototype[t+"AllElements"]=function(){return this.setAllElementStatus(a,!0),this},n.DataLayer.prototype[i+"AllElements"]=function(){return this.setAllElementStatus(a,!1),this}}),n.DataLayer.prototype.setElementStatus=function(e,a,i,s){if("undefined"==typeof e||n.DataLayer.Statuses.adjectives.indexOf(e)===-1)throw"Invalid status passed to DataLayer.setElementStatus()";if("undefined"==typeof a)throw"Invalid element passed to DataLayer.setElementStatus()";"undefined"==typeof i&&(i=!0);try{var o=this.getElementId(a)}catch(t){return this}s&&this.setAllElementStatus(e,!i),t.select("#"+o).classed("lz-data_layer-"+this.layout.type+"-"+e,i);var r=this.getElementStatusNodeId(a);null!==r&&t.select("#"+r).classed("lz-data_layer-"+this.layout.type+"-statusnode-"+e,i);var l=this.state[this.state_id][e].indexOf(o);return i&&l===-1&&this.state[this.state_id][e].push(o),i||l===-1||this.state[this.state_id][e].splice(l,1),this.showOrHideTooltip(a),this.parent.emit("layout_changed"),this.parent_plot.emit("layout_changed"),this},n.DataLayer.prototype.setElementStatusByFilters=function(t,e,a,i){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setElementStatusByFilters()";return"undefined"==typeof this.state[this.state_id][t]?this:(e="undefined"==typeof e||!!e,i="undefined"!=typeof i&&!!i,Array.isArray(a)||(a=[]),i&&this.setAllElementStatus(t,!e),this.filterElements(a).forEach(function(a){this.setElementStatus(t,a,e)}.bind(this)),this)},n.DataLayer.prototype.setAllElementStatus=function(t,e){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setAllElementStatus()";if("undefined"==typeof this.state[this.state_id][t])return this;if("undefined"==typeof e&&(e=!0),e)this.data.forEach(function(e){this.setElementStatus(t,e,!0)}.bind(this));else{var a=this.state[this.state_id][t].slice();a.forEach(function(e){var a=this.getElementById(e);"object"==typeof a&&null!==a&&this.setElementStatus(t,a,!1)}.bind(this)),this.state[this.state_id][t]=[]}return this.global_statuses[t]=e,this},n.DataLayer.prototype.applyBehaviors=function(t){"object"==typeof this.layout.behaviors&&Object.keys(this.layout.behaviors).forEach(function(e){var a=/(click|mouseover|mouseout)/.exec(e);a&&t.on(a[0]+"."+e,this.executeBehaviors(e,this.layout.behaviors[e]))}.bind(this))},n.DataLayer.prototype.executeBehaviors=function(e,a){var i={ctrl:e.indexOf("ctrl")!==-1,shift:e.indexOf("shift")!==-1};return function(e){i.ctrl===!!t.event.ctrlKey&&i.shift===!!t.event.shiftKey&&a.forEach(function(t){if("object"==typeof t&&null!==t)switch(t.action){case"set":this.setElementStatus(t.status,e,!0,t.exclusive);break;case"unset":this.setElementStatus(t.status,e,!1,t.exclusive);break;case"toggle":var a=this.state[this.state_id][t.status].indexOf(this.getElementId(e))!==-1,i=t.exclusive&&!a;this.setElementStatus(t.status,e,!a,i);break;case"link":if("string"==typeof t.href){var s=n.parseFields(e,t.href);"string"==typeof t.target?window.open(s,t.target):window.location.href=s}}}.bind(this))}.bind(this)},n.DataLayer.prototype.getPageOrigin=function(){var t=this.parent.getPageOrigin();return{x:t.x+this.parent.layout.margin.left,y:t.y+this.parent.layout.margin.top}},n.DataLayer.prototype.exportData=function(t){var e="json";t=t||e,t="string"==typeof t?t.toLowerCase():e,["json","csv","tsv"].indexOf(t)===-1&&(t=e);var a;switch(t){case"json":try{a=JSON.stringify(this.data)}catch(t){a=null,console.error("Unable to export JSON data from data layer: "+this.getBaseId()+";",t)}break;case"tsv":case"csv":try{var i=JSON.parse(JSON.stringify(this.data));if("object"!=typeof i)a=i.toString();else if(Array.isArray(i)){var n="tsv"===t?"\t":",",s=this.layout.fields.map(function(t){return JSON.stringify(t)}).join(n)+"\n";a=s+i.map(function(t){return this.layout.fields.map(function(e){return"undefined"==typeof t[e]?JSON.stringify(null):"object"==typeof t[e]&&null!==t[e]?Array.isArray(t[e])?'"[Array('+t[e].length+')]"':'"[Object]"':JSON.stringify(t[e])}).join(n)}.bind(this)).join("\n")}else a="Object"}catch(t){a=null,console.error("Unable to export CSV data from data layer: "+this.getBaseId()+";",t)}}return a},n.DataLayer.prototype.draw=function(){return this.svg.container.attr("transform","translate("+this.parent.layout.cliparea.origin.x+","+this.parent.layout.cliparea.origin.y+")"),this.svg.clipRect.attr("width",this.parent.layout.cliparea.width).attr("height",this.parent.layout.cliparea.height),this.positionAllTooltips(),this},n.DataLayer.prototype.reMap=function(){this.destroyAllTooltips();var t=this.parent_plot.lzd.getData(this.state,this.layout.fields);return t.then(function(t){this.data=t.body,this.applyDataMethods(),this.initialized=!0}.bind(this)),t},n.DataLayers=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t]){if("object"!=typeof a)throw"invalid layout argument for data layer ["+t+"]";return new e[t](a,i)}throw"data layer ["+t+"] not found"}return null},t.set=function(t,a){if(a){if("function"!=typeof a)throw"unable to set data layer ["+t+"], argument provided is not a function";e[t]=a,e[t].prototype=new n.DataLayer}else delete e[t]},t.add=function(a,i){if(e[a])throw"data layer already exists with name: "+a;t.set(a,i)},t.extend=function(t,a,i){i=i||{};var s=e[t];if(!s)throw"Attempted to subclass an unknown or unregistered datalayer type";if("object"!=typeof i)throw"Must specify an object of properties and methods";var o=n.subclass(s,i);return e[a]=o,o},t.list=function(){return Object.keys(e)},t}(),n.DataLayers.add("annotation_track",function(t){if(this.DefaultLayout={color:"#000000",filters:[]},t=n.Layouts.merge(t,this.DefaultLayout),!Array.isArray(t.filters))throw"Annotation track must specify array of filters for selecting points to annotate";return n.DataLayer.apply(this,arguments),this.render=function(){var t=this,e=this.filter(this.layout.filters,"elements"),a=this.svg.group.selectAll("rect.lz-data_layer-"+t.layout.type).data(e,function(e){return e[t.layout.id_field]});a.enter().append("rect").attr("class","lz-data_layer-"+this.layout.type).attr("id",function(e){return t.getElementId(e)}),a.attr("x",function(e){return t.parent.x_scale(e[t.layout.x_axis.field])}).attr("width",1).attr("height",t.parent.layout.height).attr("fill",function(e){return t.resolveScalableParameter(t.layout.color,e)}),a.exit().remove(),this.applyBehaviors(a)},this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=7,l=1,h=l/2,d=this.getPageOrigin(),u=o.selector.node().getBoundingClientRect(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=this.parent.x_scale(o.data[this.layout.x_axis.field]),f=p/2,g=Math.max(u.width/2-y,0),_=Math.max(u.width/2+y-c,0);a=d.x+y-u.width/2-_+g,s=u.width/2-r+_-g-h,u.height+l+r>p-f?(e=d.y+f-(u.height+l+r),i="down",n=u.height-l):(e=d.y+f+l+r,i="up",n=0-l-r),o.selector.style("left",a+"px").style("top",e+"px"),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",s+"px").style("top",n+"px")},this}),n.DataLayers.add("forest",function(e){return this.DefaultLayout={point_size:40,point_shape:"square",color:"#888888",fill_opacity:1,y_axis:{axis:2},id_field:"id",confidence_intervals:{start_field:"ci_start",end_field:"ci_end"},show_no_significance_line:!0},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n=this.tooltips[t],s=this.resolveScalableParameter(this.layout.point_size,n.data),o=7,r=1,l=6,h=this.getPageOrigin(),d=this.parent.x_scale(n.data[this.layout.x_axis.field]),u="y"+this.layout.y_axis.axis+"_scale",p=this.parent[u](n.data[this.layout.y_axis.field]),c=n.selector.node().getBoundingClientRect(),y=Math.sqrt(s/Math.PI);d<=this.parent.layout.width/2?(e=h.x+d+y+o+r,a="left",i=-1*(o+r)):(e=h.x+d-c.width-y-o-r,a="right",i=c.width-r);var f,g,_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom);p-c.height/2<=0?(f=h.y+p-1.5*o-l,g=l):p+c.height/2>=_?(f=h.y+p+o+l-c.height,g=c.height-2*o-l):(f=h.y+p-c.height/2,g=c.height/2-o),n.selector.style("left",e+"px").style("top",f+"px"),n.arrow||(n.arrow=n.selector.append("div").style("position","absolute")),n.arrow.attr("class","lz-data_layer-tooltip-arrow_"+a).style("left",i+"px").style("top",g+"px")},this.render=function(){var e="x_scale",a="y"+this.layout.y_axis.axis+"_scale";if(this.layout.confidence_intervals&&this.layout.fields.indexOf(this.layout.confidence_intervals.start_field)!==-1&&this.layout.fields.indexOf(this.layout.confidence_intervals.end_field)!==-1){var i=this.svg.group.selectAll("rect.lz-data_layer-forest.lz-data_layer-forest-ci").data(this.data,function(t){return t[this.layout.id_field]}.bind(this));i.enter().append("rect").attr("class","lz-data_layer-forest lz-data_layer-forest-ci").attr("id",function(t){return this.getElementId(t)+"_ci"}.bind(this)).attr("transform","translate(0,"+(isNaN(this.parent.layout.height)?0:this.parent.layout.height)+")");var n=function(t){var i=this.parent[e](t[this.layout.confidence_intervals.start_field]),n=this.parent[a](t[this.layout.y_axis.field]);return isNaN(i)&&(i=-1e3),isNaN(n)&&(n=-1e3),"translate("+i+","+n+")"}.bind(this),s=function(t){return this.parent[e](t[this.layout.confidence_intervals.end_field])-this.parent[e](t[this.layout.confidence_intervals.start_field])}.bind(this),o=1;this.canTransition()?i.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",n).attr("width",s).attr("height",o):i.attr("transform",n).attr("width",s).attr("height",o),i.exit().remove()}var r=this.svg.group.selectAll("path.lz-data_layer-forest.lz-data_layer-forest-point").data(this.data,function(t){return t[this.layout.id_field]}.bind(this)),l=isNaN(this.parent.layout.height)?0:this.parent.layout.height;r.enter().append("path").attr("class","lz-data_layer-forest lz-data_layer-forest-point").attr("id",function(t){return this.getElementId(t)+"_point"}.bind(this)).attr("transform","translate(0,"+l+")");var h=function(t){var i=this.parent[e](t[this.layout.x_axis.field]),n=this.parent[a](t[this.layout.y_axis.field]);return isNaN(i)&&(i=-1e3),isNaN(n)&&(n=-1e3),"translate("+i+","+n+")"}.bind(this),d=function(t){return this.resolveScalableParameter(this.layout.color,t)}.bind(this),u=function(t){return this.resolveScalableParameter(this.layout.fill_opacity,t)}.bind(this),p=t.svg.symbol().size(function(t){return this.resolveScalableParameter(this.layout.point_size,t)}.bind(this)).type(function(t){return this.resolveScalableParameter(this.layout.point_shape,t)}.bind(this));this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p):r.attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p),r.exit().remove(),r.on("click.event_emitter",function(t){this.parent.emit("element_clicked",t),this.parent_plot.emit("element_clicked",t)}.bind(this)),this.applyBehaviors(r)},this}),n.DataLayers.add("genes",function(e){return this.DefaultLayout={label_font_size:12,label_exon_spacing:4,exon_height:16,bounding_box_padding:6,track_vertical_spacing:10},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.getElementStatusNodeId=function(t){return this.getElementId(t)+"-statusnode"},this.getTrackHeight=function(){return 2*this.layout.bounding_box_padding+this.layout.label_font_size+this.layout.label_exon_spacing+this.layout.exon_height+this.layout.track_vertical_spacing},this.transcript_idx=0,this.tracks=1,this.gene_track_index={1:[]},this.assignTracks=function(){return this.getLabelWidth=function(t,e){try{var a=this.svg.group.append("text").attr("x",0).attr("y",0).attr("class","lz-data_layer-genes lz-label").style("font-size",e).text(t+"→"),i=a.node().getBBox().width;return a.remove(),i}catch(t){return 0}},this.tracks=1,this.gene_track_index={1:[]},this.data.map(function(t,e){if(this.data[e].gene_id&&this.data[e].gene_id.indexOf(".")){var a=this.data[e].gene_id.split(".");this.data[e].gene_id=a[0],this.data[e].gene_version=a[1]}if(this.data[e].transcript_id=this.data[e].transcripts[this.transcript_idx].transcript_id,this.data[e].display_range={start:this.parent.x_scale(Math.max(t.start,this.state.start)),end:this.parent.x_scale(Math.min(t.end,this.state.end))},this.data[e].display_range.label_width=this.getLabelWidth(this.data[e].gene_name,this.layout.label_font_size),this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start,this.data[e].display_range.text_anchor="middle",this.data[e].display_range.widththis.state.end)this.data[e].display_range.start=this.data[e].display_range.end-this.data[e].display_range.label_width-this.layout.label_font_size,this.data[e].display_range.text_anchor="end";else{var i=(this.data[e].display_range.label_width-this.data[e].display_range.width)/2+this.layout.label_font_size;this.data[e].display_range.start-ithis.parent.x_scale(this.state.end)?(this.data[e].display_range.end=this.parent.x_scale(this.state.end),this.data[e].display_range.start=this.data[e].display_range.end-this.data[e].display_range.label_width,this.data[e].display_range.text_anchor="end"):(this.data[e].display_range.start-=i,this.data[e].display_range.end+=i)}this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start}this.data[e].display_range.start-=this.layout.bounding_box_padding,this.data[e].display_range.end+=this.layout.bounding_box_padding,this.data[e].display_range.width+=2*this.layout.bounding_box_padding,this.data[e].display_domain={start:this.parent.x_scale.invert(this.data[e].display_range.start),end:this.parent.x_scale.invert(this.data[e].display_range.end)},this.data[e].display_domain.width=this.data[e].display_domain.end-this.data[e].display_domain.start,this.data[e].track=null;for(var n=1;null===this.data[e].track;){var s=!1;this.gene_track_index[n].map(function(t){if(!s){var e=Math.min(t.display_range.start,this.display_range.start),a=Math.max(t.display_range.end,this.display_range.end);a-ethis.tracks&&(this.tracks=n,this.gene_track_index[n]=[])):(this.data[e].track=n,this.gene_track_index[n].push(this.data[e]))}this.data[e].parent=this,this.data[e].transcripts.map(function(t,a){this.data[e].transcripts[a].parent=this.data[e],this.data[e].transcripts[a].exons.map(function(t,i){this.data[e].transcripts[a].exons[i].parent=this.data[e].transcripts[a]}.bind(this))}.bind(this))}.bind(this)),this},this.render=function(){this.assignTracks();var e,a,i,n,s=this.svg.group.selectAll("g.lz-data_layer-genes").data(this.data,function(t){return t.gene_name});s.enter().append("g").attr("class","lz-data_layer-genes"),s.attr("id",function(t){return this.getElementId(t)}.bind(this)).each(function(s){var o=s.parent,r=t.select(this).selectAll("rect.lz-data_layer-genes.lz-data_layer-genes-statusnode").data([s],function(t){return o.getElementStatusNodeId(t)});r.enter().append("rect").attr("class","lz-data_layer-genes lz-data_layer-genes-statusnode"),r.attr("id",function(t){return o.getElementStatusNodeId(t)}).attr("rx",function(){return o.layout.bounding_box_padding}).attr("ry",function(){return o.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return o.getTrackHeight()-o.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*o.getTrackHeight()},o.canTransition()?r.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):r.attr("width",e).attr("height",a).attr("x",i).attr("y",n),r.exit().remove();var l=t.select(this).selectAll("rect.lz-data_layer-genes.lz-boundary").data([s],function(t){return t.gene_name+"_boundary"});l.enter().append("rect").attr("class","lz-data_layer-genes lz-boundary"),e=function(t){return o.parent.x_scale(t.end)-o.parent.x_scale(t.start)},a=function(){return 1},i=function(t){return o.parent.x_scale(t.start)},n=function(t){return(t.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size+o.layout.label_exon_spacing+Math.max(o.layout.exon_height,3)/2},o.canTransition()?l.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):l.attr("width",e).attr("height",a).attr("x",i).attr("y",n),l.exit().remove();var h=t.select(this).selectAll("text.lz-data_layer-genes.lz-label").data([s],function(t){return t.gene_name+"_label"});h.enter().append("text").attr("class","lz-data_layer-genes lz-label"),h.attr("text-anchor",function(t){return t.display_range.text_anchor}).text(function(t){return"+"===t.strand?t.gene_name+"→":"←"+t.gene_name}).style("font-size",s.parent.layout.label_font_size),i=function(t){return"middle"===t.display_range.text_anchor?t.display_range.start+t.display_range.width/2:"start"===t.display_range.text_anchor?t.display_range.start+o.layout.bounding_box_padding:"end"===t.display_range.text_anchor?t.display_range.end-o.layout.bounding_box_padding:void 0},n=function(t){return(t.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size},o.canTransition()?h.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("x",i).attr("y",n):h.attr("x",i).attr("y",n),h.exit().remove();var d=t.select(this).selectAll("rect.lz-data_layer-genes.lz-exon").data(s.transcripts[s.parent.transcript_idx].exons,function(t){return t.exon_id});d.enter().append("rect").attr("class","lz-data_layer-genes lz-exon"),e=function(t){return o.parent.x_scale(t.end)-o.parent.x_scale(t.start)},a=function(){return o.layout.exon_height},i=function(t){return o.parent.x_scale(t.start)},n=function(){return(s.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size+o.layout.label_exon_spacing},o.canTransition()?d.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):d.attr("width",e).attr("height",a).attr("x",i).attr("y",n),d.exit().remove();var u=t.select(this).selectAll("rect.lz-data_layer-genes.lz-clickarea").data([s],function(t){return t.gene_name+"_clickarea"});u.enter().append("rect").attr("class","lz-data_layer-genes lz-clickarea"),u.attr("id",function(t){return o.getElementId(t)+"_clickarea"}).attr("rx",function(){return o.layout.bounding_box_padding}).attr("ry",function(){return o.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return o.getTrackHeight()-o.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*o.getTrackHeight()},o.canTransition()?u.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):u.attr("width",e).attr("height",a).attr("x",i).attr("y",n),u.exit().remove(),u.on("click.event_emitter",function(t){t.parent.parent.emit("element_clicked",t),t.parent.parent_plot.emit("element_clicked",t)}),o.applyBehaviors(u)}),s.exit().remove()},this.positionTooltip=function(e){if("string"!=typeof e)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[e])throw"Unable to position tooltip: id does not point to a valid tooltip";var a,i,n,s=this.tooltips[e],o=7,r=1,l=this.getPageOrigin(),h=s.selector.node().getBoundingClientRect(),d=this.getElementStatusNodeId(s.data),u=t.select("#"+d).node().getBBox(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=(s.data.display_range.start+s.data.display_range.end)/2-this.layout.bounding_box_padding/2,f=Math.max(h.width/2-y,0),g=Math.max(h.width/2+y-c,0),_=l.x+y-h.width/2-g+f,m=h.width/2-o/2+g-f;h.height+r+o>p-(u.y+u.height)?(a=l.y+u.y-(h.height+r+o),i="down",n=h.height-r):(a=l.y+u.y+u.height+r+o,i="up",n=0-r-o),s.selector.style("left",_+"px").style("top",a+"px"),s.arrow||(s.arrow=s.selector.append("div").style("position","absolute")),s.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",m+"px").style("top",n+"px")},this}),n.DataLayers.add("genome_legend",function(t){return this.DefaultLayout={chromosome_fill_colors:{light:"rgb(155, 155, 188)",dark:"rgb(95, 95, 128)"},chromosome_label_colors:{light:"rgb(120, 120, 186)",dark:"rgb(0, 0, 66)"}},t=n.Layouts.merge(t,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.render=function(){var t=0;this.data.forEach(function(e,a){this.data[a].genome_start=t,this.data[a].genome_end=t+e["genome:base_pairs"],t+=e["genome:base_pairs"]}.bind(this));var e=this.svg.group.selectAll("rect.lz-data_layer-genome_legend").data(this.data,function(t){return t["genome:chr"]});e.enter().append("rect").attr("class","lz-data_layer-genome_legend");var a=this,i=this.parent;e.attr("fill",function(t){ -return t["genome:chr"]%2?a.layout.chromosome_fill_colors.light:a.layout.chromosome_fill_colors.dark}).attr("x",function(t){return i.x_scale(t.genome_start)}).attr("y",0).attr("width",function(t){return i.x_scale(t["genome:base_pairs"])}).attr("height",i.layout.cliparea.height),e.exit().remove();var n=/([^:]+):(\d+)(?:_.*)?/.exec(this.state.variant);if(!n)throw"Genome legend cannot understand the specified variant position";var s=n[1],o=n[2];t=+this.data[s-1].genome_start+ +o;var r=this.svg.group.selectAll("rect.lz-data_layer-genome_legend-marker").data([{start:t,end:t+1}]);r.enter().append("rect").attr("class","lz-data_layer-genome_legend-marker"),r.transition().duration(500).style({fill:"rgba(255, 250, 50, 0.8)",stroke:"rgba(255, 250, 50, 0.8)","stroke-width":"3px"}).attr("x",function(t){return i.x_scale(t.start)}).attr("y",0).attr("width",function(t){return i.x_scale(t.end-t.start)}).attr("height",i.layout.cliparea.height),r.exit().remove()},this}),n.DataLayers.add("intervals",function(e){return this.DefaultLayout={start_field:"start",end_field:"end",track_split_field:"state_id",track_split_order:"DESC",track_split_legend_to_y_axis:2,split_tracks:!0,track_height:15,track_vertical_spacing:3,bounding_box_padding:2,always_hide_legend:!1,color:"#B8B8B8",fill_opacity:1},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.getElementStatusNodeId=function(t){return this.layout.split_tracks?(this.getBaseId()+"-statusnode-"+t[this.layout.track_split_field]).replace(/[:.[\],]/g,"_"):this.getElementId(t)+"-statusnode"}.bind(this),this.getTrackHeight=function(){return this.layout.track_height+this.layout.track_vertical_spacing+2*this.layout.bounding_box_padding},this.tracks=1,this.previous_tracks=1,this.interval_track_index={1:[]},this.assignTracks=function(){if(this.previous_tracks=this.tracks,this.tracks=0,this.interval_track_index={1:[]},this.track_split_field_index={},this.layout.track_split_field&&this.layout.split_tracks){this.data.map(function(t){this.track_split_field_index[t[this.layout.track_split_field]]=null}.bind(this));var t=Object.keys(this.track_split_field_index);"DESC"===this.layout.track_split_order&&t.reverse(),t.forEach(function(t){this.track_split_field_index[t]=this.tracks+1,this.interval_track_index[this.tracks+1]=[],this.tracks++}.bind(this))}return this.data.map(function(t,e){if(this.data[e].parent=this,this.data[e].display_range={start:this.parent.x_scale(Math.max(t[this.layout.start_field],this.state.start)),end:this.parent.x_scale(Math.min(t[this.layout.end_field],this.state.end))},this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start,this.data[e].display_domain={start:this.parent.x_scale.invert(this.data[e].display_range.start),end:this.parent.x_scale.invert(this.data[e].display_range.end)},this.data[e].display_domain.width=this.data[e].display_domain.end-this.data[e].display_domain.start,this.layout.track_split_field&&this.layout.split_tracks){var a=this.data[e][this.layout.track_split_field];this.data[e].track=this.track_split_field_index[a],this.interval_track_index[this.data[e].track].push(e)}else{this.tracks=1,this.data[e].track=null;for(var i=1;null===this.data[e].track;){var n=!1;this.interval_track_index[i].map(function(t){if(!n){var e=Math.min(t.display_range.start,this.display_range.start),a=Math.max(t.display_range.end,this.display_range.end);a-ethis.tracks&&(this.tracks=i,this.interval_track_index[i]=[])):(this.data[e].track=i,this.interval_track_index[i].push(this.data[e]))}}}.bind(this)),this},this.render=function(){this.assignTracks(),this.svg.group.selectAll(".lz-data_layer-intervals-statusnode.lz-data_layer-intervals-shared").remove(),Object.keys(this.track_split_field_index).forEach(function(t){var e={};e[this.layout.track_split_field]=t;var a={display:this.layout.split_tracks?null:"none"};this.svg.group.insert("rect",":first-child").attr("id",this.getElementStatusNodeId(e)).attr("class","lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-shared").attr("rx",this.layout.bounding_box_padding).attr("ry",this.layout.bounding_box_padding).attr("width",this.parent.layout.cliparea.width).attr("height",this.getTrackHeight()-this.layout.track_vertical_spacing).attr("x",0).attr("y",(this.track_split_field_index[t]-1)*this.getTrackHeight()).style(a)}.bind(this));var e,a,i,n,s,o,r=this.svg.group.selectAll("g.lz-data_layer-intervals").data(this.data,function(t){return t[this.layout.id_field]}.bind(this));return r.enter().append("g").attr("class","lz-data_layer-intervals"),r.attr("id",function(t){return this.getElementId(t)}.bind(this)).each(function(r){var l=r.parent,h={display:l.layout.split_tracks?"none":null},d=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-statusnode-discrete").data([r],function(t){return l.getElementId(t)+"-statusnode"});d.enter().insert("rect",":first-child").attr("class","lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-statusnode-discrete"),d.attr("id",function(t){return l.getElementId(t)+"-statusnode"}).attr("rx",function(){return l.layout.bounding_box_padding}).attr("ry",function(){return l.layout.bounding_box_padding}).style(h),e=function(t){return t.display_range.width+2*l.layout.bounding_box_padding},a=function(){return l.getTrackHeight()-l.layout.track_vertical_spacing},i=function(t){return t.display_range.start-l.layout.bounding_box_padding},n=function(t){return(t.track-1)*l.getTrackHeight()},l.canTransition()?d.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):d.attr("width",e).attr("height",a).attr("x",i).attr("y",n),d.exit().remove();var u=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-interval_rect").data([r],function(t){return t[l.layout.id_field]+"_interval_rect"});u.enter().append("rect").attr("class","lz-data_layer-intervals lz-interval_rect"),a=l.layout.track_height,e=function(t){return t.display_range.width},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*l.getTrackHeight()+l.layout.bounding_box_padding},s=function(t){return l.resolveScalableParameter(l.layout.color,t)},o=function(t){return l.resolveScalableParameter(l.layout.fill_opacity,t)},l.canTransition()?u.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n).attr("fill",s).attr("fill-opacity",o):u.attr("width",e).attr("height",a).attr("x",i).attr("y",n).attr("fill",s).attr("fill-opacity",o),u.exit().remove();var p=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-clickarea").data([r],function(t){return t.interval_name+"_clickarea"});p.enter().append("rect").attr("class","lz-data_layer-intervals lz-clickarea"),p.attr("id",function(t){return l.getElementId(t)+"_clickarea"}).attr("rx",function(){return l.layout.bounding_box_padding}).attr("ry",function(){return l.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return l.getTrackHeight()-l.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*l.getTrackHeight()},l.canTransition()?p.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):p.attr("width",e).attr("height",a).attr("x",i).attr("y",n),p.exit().remove(),p.on("click",function(t){t.parent.parent.emit("element_clicked",t),t.parent.parent_plot.emit("element_clicked",t)}.bind(this)),l.applyBehaviors(p)}),r.exit().remove(),this.previous_tracks!==this.tracks&&this.updateSplitTrackAxis(),this},this.positionTooltip=function(e){if("string"!=typeof e)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[e])throw"Unable to position tooltip: id does not point to a valid tooltip";var a,i,n,s=this.tooltips[e],o=7,r=1,l=this.getPageOrigin(),h=s.selector.node().getBoundingClientRect(),d=t.select("#"+this.getElementStatusNodeId(s.data)).node().getBBox(),u=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),p=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),c=(s.data.display_range.start+s.data.display_range.end)/2-this.layout.bounding_box_padding/2,y=Math.max(h.width/2-c,0),f=Math.max(h.width/2+c-p,0),g=l.x+c-h.width/2-f+y,_=h.width/2-o/2+f-y;h.height+r+o>u-(d.y+d.height)?(a=l.y+d.y-(h.height+r+o),i="down",n=h.height-r):(a=l.y+d.y+d.height+r+o,i="up",n=0-r-o),s.selector.style("left",g+"px").style("top",a+"px"),s.arrow||(s.arrow=s.selector.append("div").style("position","absolute")),s.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",_+"px").style("top",n+"px")},this.updateSplitTrackAxis=function(){var t=!!this.layout.track_split_legend_to_y_axis&&"y"+this.layout.track_split_legend_to_y_axis;if(this.layout.split_tracks){var e=+this.tracks||0,a=+this.layout.track_height||0,i=2*(+this.layout.bounding_box_padding||0)+(+this.layout.track_vertical_spacing||0),n=e*a+(e-1)*i;this.parent.scaleHeightToData(n),t&&this.parent.legend&&(this.parent.legend.hide(),this.parent.layout.axes[t]={render:!0,ticks:[],range:{start:n-this.layout.track_height/2,end:this.layout.track_height/2}},this.layout.legend.forEach(function(a){var i=a[this.layout.track_split_field],n=this.track_split_field_index[i];n&&("DESC"===this.layout.track_split_order&&(n=Math.abs(n-e-1)),this.parent.layout.axes[t].ticks.push({y:n,text:a.label}))}.bind(this)),this.layout.y_axis={axis:this.layout.track_split_legend_to_y_axis,floor:1,ceiling:e},this.parent.render()),this.parent_plot.positionPanels()}else t&&this.parent.legend&&(this.layout.always_hide_legend||this.parent.legend.show(),this.parent.layout.axes[t]={render:!1},this.parent.render());return this},this.toggleSplitTracks=function(){return this.layout.split_tracks=!this.layout.split_tracks,this.parent.legend&&!this.layout.always_hide_legend&&(this.parent.layout.margin.bottom=5+(this.layout.split_tracks?0:this.parent.legend.layout.height+5)),this.render(),this.updateSplitTrackAxis(),this},this}),n.DataLayers.add("line",function(e){return this.DefaultLayout={style:{fill:"none","stroke-width":"2px"},interpolate:"linear",x_axis:{field:"x"},y_axis:{field:"y",axis:1},hitarea_width:5},e=n.Layouts.merge(e,this.DefaultLayout),this.mouse_event=null,this.line=null,this.tooltip_timeout=null,n.DataLayer.apply(this,arguments),this.getMouseDisplayAndData=function(){var e={display:{x:t.mouse(this.mouse_event)[0],y:null},data:{},slope:null},a=this.layout.x_axis.field,i=this.layout.y_axis.field,n="x_scale",s="y"+this.layout.y_axis.axis+"_scale";e.data[a]=this.parent[n].invert(e.display.x);var o=t.bisector(function(t){return+t[a]}).left,r=o(this.data,e.data[a])-1,l=this.data[r],h=this.data[r+1],d=t.interpolateNumber(+l[i],+h[i]),u=+h[a]-+l[a];return e.data[i]=d(e.data[a]%u/u),e.display.y=this.parent[s](e.data[i]),this.layout.tooltip.x_precision&&(e.data[a]=e.data[a].toPrecision(this.layout.tooltip.x_precision)),this.layout.tooltip.y_precision&&(e.data[i]=e.data[i].toPrecision(this.layout.tooltip.y_precision)),e.slope=(this.parent[s](h[i])-this.parent[s](l[i]))/(this.parent[n](h[a])-this.parent[n](l[a])),e},this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=o.selector.node().getBoundingClientRect(),l=7,h=6,d=parseFloat(this.layout.style["stroke-width"])||1,u=this.getPageOrigin(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=this.getMouseDisplayAndData();if(Math.abs(y.slope)>1)y.display.x<=this.parent.layout.width/2?(a=u.x+y.display.x+d+l+d,s="left",n=-1*(l+d)):(a=u.x+y.display.x-r.width-d-l-d,s="right",n=r.width-d),y.display.y-r.height/2<=0?(e=u.y+y.display.y-1.5*l-h,i=h):y.display.y+r.height/2>=p?(e=u.y+y.display.y+l+h-r.height,i=r.height-2*l-h):(e=u.y+y.display.y-r.height/2,i=r.height/2-l);else{var f=Math.max(r.width/2-y.display.x,0),g=Math.max(r.width/2+y.display.x-c,0);a=u.x+y.display.x-r.width/2-g+f;var _=l/2,m=r.width-2.5*l;n=r.width/2-l+g-f,n=Math.min(Math.max(n,_),m),r.height+d+l>y.display.y?(e=u.y+y.display.y+d+l,s="up",i=0-d-l):(e=u.y+y.display.y-(r.height+d+l),s="down",i=r.height-d)}o.selector.style({left:a+"px",top:e+"px"}),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+s).style({left:n+"px",top:i+"px"})},this.render=function(){var e=this,a=this.parent,i=this.layout.x_axis.field,n=this.layout.y_axis.field,s="x_scale",o="y"+this.layout.y_axis.axis+"_scale",r=this.svg.group.selectAll("path.lz-data_layer-line").data([this.data]);if(this.path=r.enter().append("path").attr("class","lz-data_layer-line"),this.line=t.svg.line().x(function(t){return parseFloat(a[s](t[i]))}).y(function(t){return parseFloat(a[o](t[n]))}).interpolate(this.layout.interpolate),this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("d",this.line).style(this.layout.style):r.attr("d",this.line).style(this.layout.style),this.layout.tooltip){var l=parseFloat(this.layout.hitarea_width).toString()+"px",h=this.svg.group.selectAll("path.lz-data_layer-line-hitarea").data([this.data]);h.enter().append("path").attr("class","lz-data_layer-line-hitarea").style("stroke-width",l);var d=t.svg.line().x(function(t){return parseFloat(a[s](t[i]))}).y(function(t){return parseFloat(a[o](t[n]))}).interpolate(this.layout.interpolate);h.attr("d",d).on("mouseover",function(){clearTimeout(e.tooltip_timeout),e.mouse_event=this;var t=e.getMouseDisplayAndData();e.createTooltip(t.data)}).on("mousemove",function(){clearTimeout(e.tooltip_timeout),e.mouse_event=this;var t=e.getMouseDisplayAndData();e.updateTooltip(t.data),e.positionTooltip(e.getElementId())}).on("mouseout",function(){e.tooltip_timeout=setTimeout(function(){e.mouse_event=null,e.destroyTooltip(e.getElementId())},300)}),h.exit().remove()}r.exit().remove()},this.setElementStatus=function(t,e,a){return this.setAllElementStatus(t,a)},this.setElementStatusByFilters=function(t,e){return this.setAllElementStatus(t,e)},this.setAllElementStatus=function(t,e){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setAllElementStatus()";if("undefined"==typeof this.state[this.state_id][t])return this;"undefined"==typeof e&&(e=!0),this.global_statuses[t]=e;var a="lz-data_layer-line";return Object.keys(this.global_statuses).forEach(function(t){this.global_statuses[t]&&(a+=" lz-data_layer-line-"+t)}.bind(this)),this.path.attr("class",a),this.parent.emit("layout_changed"),this.parent_plot.emit("layout_changed"),this},this}),n.DataLayers.add("orthogonal_line",function(e){return this.DefaultLayout={style:{stroke:"#D3D3D3","stroke-width":"3px","stroke-dasharray":"10px 10px"},orientation:"horizontal",x_axis:{axis:1,decoupled:!0},y_axis:{axis:1,decoupled:!0},offset:0},e=n.Layouts.merge(e,this.DefaultLayout),["horizontal","vertical"].indexOf(e.orientation)===-1&&(e.orientation="horizontal"),this.data=[],this.line=null,n.DataLayer.apply(this,arguments),this.render=function(){var e=this.parent,a="x_scale",i="y"+this.layout.y_axis.axis+"_scale",n="x_extent",s="y"+this.layout.y_axis.axis+"_extent",o="x_range",r="y"+this.layout.y_axis.axis+"_range";"horizontal"===this.layout.orientation?this.data=[{x:e[n][0],y:this.layout.offset},{x:e[n][1],y:this.layout.offset}]:this.data=[{x:this.layout.offset,y:e[s][0]},{x:this.layout.offset,y:e[s][1]}];var l=this.svg.group.selectAll("path.lz-data_layer-line").data([this.data]);this.path=l.enter().append("path").attr("class","lz-data_layer-line"),this.line=t.svg.line().x(function(t,i){var n=parseFloat(e[a](t.x));return isNaN(n)?e[o][i]:n}).y(function(t,a){var n=parseFloat(e[i](t.y));return isNaN(n)?e[r][a]:n}).interpolate("linear"),this.canTransition()?l.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("d",this.line).style(this.layout.style):l.attr("d",this.line).style(this.layout.style),l.exit().remove()},this}),n.DataLayers.add("scatter",function(e){return this.DefaultLayout={point_size:40,point_shape:"circle",tooltip_positioning:"horizontal",color:"#888888",fill_opacity:1,y_axis:{axis:1},id_field:"id"},e=n.Layouts.merge(e,this.DefaultLayout),e.label&&isNaN(e.label.spacing)&&(e.label.spacing=4),n.DataLayer.apply(this,arguments),this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=this.resolveScalableParameter(this.layout.point_size,o.data),l=Math.sqrt(r/Math.PI),h=7,d=1,u=6,p=this.getPageOrigin(),c=this.parent.x_scale(o.data[this.layout.x_axis.field]),y="y"+this.layout.y_axis.axis+"_scale",f=this.parent[y](o.data[this.layout.y_axis.field]),g=o.selector.node().getBoundingClientRect(),_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),m=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right);if("vertical"===this.layout.tooltip_positioning){var b=Math.max(g.width/2-c,0),x=Math.max(g.width/2+c-m,0);a=p.x+c-g.width/2-x+b,s=g.width/2-h/2+x-b-l,g.height+d+h>_-(f+l)?(e=p.y+f-(l+g.height+d+h),i="down",n=g.height-d):(e=p.y+f+l+d+h,i="up",n=0-d-h)}else c<=this.parent.layout.width/2?(a=p.x+c+l+h+d,i="left",s=-1*(h+d)):(a=p.x+c-g.width-l-h-d,i="right",s=g.width-d),_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),f-g.height/2<=0?(e=p.y+f-1.5*h-u,n=u):f+g.height/2>=_?(e=p.y+f+h+u-g.height,n=g.height-2*h-u):(e=p.y+f-g.height/2,n=g.height/2-h);o.selector.style("left",a+"px").style("top",e+"px"),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",s+"px").style("top",n+"px")},this.flip_labels=function(){var e=this,a=e.resolveScalableParameter(e.layout.point_size,{}),i=e.layout.label.spacing,n=Boolean(e.layout.label.lines),s=2*i,o=e.parent.layout.width-e.parent.layout.margin.left-e.parent.layout.margin.right-2*i,r=function(t,e){var s=+t.attr("x"),o=2*i+2*Math.sqrt(a);if(n)var r=+e.attr("x2"),l=i+2*Math.sqrt(a);"start"===t.style("text-anchor")?(t.style("text-anchor","end"),t.attr("x",s-o),n&&e.attr("x2",r-l)):(t.style("text-anchor","start"),t.attr("x",s+o),n&&e.attr("x2",r+l))};e.label_texts.each(function(a,s){var l=this,h=t.select(l),d=+h.attr("x"),u=h.node().getBoundingClientRect();if(d+u.width+i>o){var p=n?t.select(e.label_lines[0][s]):null;r(h,p)}}),e.label_texts.each(function(a,o){var l=this,h=t.select(l);if("end"!==h.style("text-anchor")){var d=+h.attr("x"),u=h.node().getBoundingClientRect(),p=n?t.select(e.label_lines[0][o]):null;e.label_texts.each(function(){var e=this,a=t.select(e),n=a.node().getBoundingClientRect(),o=u.leftn.left&&u.topn.top;o&&(r(h,p),d=+h.attr("x"),d-u.width-iu.left&&d.topu.top;if(p){n=!0;var c,y=h.attr("y"),f=d.topx?(c=_-+r,_=+r,m-=c):m+u.height/2>x&&(c=m-+y,m=+y,_-=c),o.attr("y",_),h.attr("y",m)}}}})}),n){if(e.layout.label.lines){var s=e.label_texts[0];e.label_lines.attr("y2",function(e,a){var i=t.select(s[a]);return i.attr("y")})}this.seperate_iterations<150&&setTimeout(function(){this.separate_labels()}.bind(this),1)}},this.render=function(){var e=this,a="x_scale",i="y"+this.layout.y_axis.axis+"_scale";if(this.layout.label){var s=this.data.filter(function(t){if(e.layout.label.filters){var a=!0;return e.layout.label.filters.forEach(function(e){var i=new n.Data.Field(e.field).resolve(t);if(isNaN(i))a=!1;else switch(e.operator){case"<":i":i>e.value||(a=!1);break;case">=":i>=e.value||(a=!1);break;case"=":i!==e.value&&(a=!1);break;default:a=!1}}),a}return!0}),o=this;this.label_groups=this.svg.group.selectAll("g.lz-data_layer-"+this.layout.type+"-label").data(s,function(t){return t[o.layout.id_field]+"_label"}),this.label_groups.enter().append("g").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_texts&&this.label_texts.remove(),this.label_texts=this.label_groups.append("text").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_texts.text(function(t){return n.parseFields(t,e.layout.label.text||"")}).style(e.layout.label.style||{}).attr({x:function(t){var i=e.parent[a](t[e.layout.x_axis.field])+Math.sqrt(e.resolveScalableParameter(e.layout.point_size,t))+e.layout.label.spacing;return isNaN(i)&&(i=-1e3),i},y:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a},"text-anchor":function(){return"start"}}),e.layout.label.lines&&(this.label_lines&&this.label_lines.remove(),this.label_lines=this.label_groups.append("line").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_lines.style(e.layout.label.lines.style||{}).attr({x1:function(t){var i=e.parent[a](t[e.layout.x_axis.field]);return isNaN(i)&&(i=-1e3),i},y1:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a},x2:function(t){var i=e.parent[a](t[e.layout.x_axis.field])+Math.sqrt(e.resolveScalableParameter(e.layout.point_size,t))+e.layout.label.spacing/2;return isNaN(i)&&(i=-1e3),i},y2:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a}})),this.label_groups.exit().remove()}var r=this.svg.group.selectAll("path.lz-data_layer-"+this.layout.type).data(this.data,function(t){return t[this.layout.id_field]}.bind(this)),l=isNaN(this.parent.layout.height)?0:this.parent.layout.height;r.enter().append("path").attr("class","lz-data_layer-"+this.layout.type).attr("id",function(t){return this.getElementId(t)}.bind(this)).attr("transform","translate(0,"+l+")");var h=function(t){var e=this.parent[a](t[this.layout.x_axis.field]),n=this.parent[i](t[this.layout.y_axis.field]);return isNaN(e)&&(e=-1e3),isNaN(n)&&(n=-1e3),"translate("+e+","+n+")"}.bind(this),d=function(t){return this.resolveScalableParameter(this.layout.color,t)}.bind(this),u=function(t){return this.resolveScalableParameter(this.layout.fill_opacity,t)}.bind(this),p=t.svg.symbol().size(function(t){return this.resolveScalableParameter(this.layout.point_size,t)}.bind(this)).type(function(t){return this.resolveScalableParameter(this.layout.point_shape,t)}.bind(this));this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p):r.attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p),r.exit().remove(),r.on("click.event_emitter",function(t){this.parent.emit("element_clicked",t),this.parent_plot.emit("element_clicked",t)}.bind(this)),this.applyBehaviors(r),this.layout.label&&(this.flip_labels(),this.seperate_iterations=0,this.separate_labels(),this.applyBehaviors(this.label_texts))},this.makeLDReference=function(t){var e=null;if("undefined"==typeof t)throw"makeLDReference requires one argument of any type";e="object"==typeof t?this.layout.id_field&&"undefined"!=typeof t[this.layout.id_field]?t[this.layout.id_field].toString():"undefined"!=typeof t.id?t.id.toString():t.toString():t.toString(),this.parent_plot.applyState({ldrefvar:e})},this}),n.DataLayers.extend("scatter","category_scatter",{_prepareData:function(){var t=this.layout.x_axis.field||"x",e=this.layout.x_axis.category_field;if(!e)throw"Layout for "+this.layout.id+" must specify category_field";var a=this.data.sort(function(t,a){var i=t[e],n=a[e],s=i.toString?i.toString().toLowerCase():i,o=n.toString?n.toString().toLowerCase():n;return s===o?0:s1?function(t){for(var e=t,n=0;n1?Math.ceil(Math.log(t)/Math.LN10):Math.floor(Math.log(t)/Math.LN10),Math.abs(e)<=3?t.toFixed(3):t.toExponential(2).replace("+","").replace("e"," × 10^")}),n.TransformationFunctions.add("urlencode",function(t){return encodeURIComponent(t)}),n.TransformationFunctions.add("htmlescape",function(t){return t?(t+="",t.replace(/['"<>&`]/g,function(t){switch(t){case"'":return"'";case'"':return""";case"<":return"<";case">":return">";case"&":return"&";case"`":return"`"}})):""}),n.ScaleFunctions=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t])return"undefined"==typeof a&&"undefined"==typeof i?e[t]:e[t](a,i);throw"scale function ["+t+"] not found"}return null},t.set=function(t,a){a?e[t]=a:delete e[t]},t.add=function(a,i){if(e[a])throw"scale function already exists with name: "+a;t.set(a,i)},t.list=function(){return Object.keys(e)},t}(),n.ScaleFunctions.add("if",function(t,e){return"undefined"==typeof e||t.field_value!==e?"undefined"!=typeof t.else?t.else:null:t.then}),n.ScaleFunctions.add("numerical_bin",function(t,e){var a=t.breaks||[],i=t.values||[];if("undefined"==typeof e||null===e||isNaN(+e))return t.null_value?t.null_value:null;var n=a.reduce(function(t,a){return+e=t&&+e=e.breaks[e.breaks.length-1])return n[i.length-1];var o=null;if(i.forEach(function(t,e){e&&i[e-1]<=+a&&i[e]>=+a&&(o=e)}),null===o)return s;var r=(+a-i[o-1])/(i[o]-i[o-1]);return isFinite(r)?t.interpolate(n[o-1],n[o])(r):s}),n.Dashboard=function(t){if(!(t instanceof n.Plot||t instanceof n.Panel))throw"Unable to create dashboard, parent must be a locuszoom plot or panel";return this.parent=t,this.id=this.parent.getBaseId()+".dashboard",this.type=this.parent instanceof n.Plot?"plot":"panel",this.parent_plot="plot"===this.type?this.parent:this.parent.parent,this.selector=null,this.components=[],this.hide_timeout=null,this.persist=!1,this.initialize()},n.Dashboard.prototype.initialize=function(){return Array.isArray(this.parent.layout.dashboard.components)&&this.parent.layout.dashboard.components.forEach(function(t){try{var e=n.Dashboard.Components.get(t.type,t,this);this.components.push(e)}catch(t){console.warn(t)}}.bind(this)),"panel"===this.type&&(t.select(this.parent.parent.svg.node().parentNode).on("mouseover."+this.id,function(){clearTimeout(this.hide_timeout),this.selector&&"hidden"!==this.selector.style("visibility")||this.show()}.bind(this)),t.select(this.parent.parent.svg.node().parentNode).on("mouseout."+this.id,function(){clearTimeout(this.hide_timeout),this.hide_timeout=setTimeout(function(){this.hide()}.bind(this),300)}.bind(this))),this},n.Dashboard.prototype.shouldPersist=function(){if(this.persist)return!0;var t=!1;return this.components.forEach(function(e){t=t||e.shouldPersist()}),t=t||this.parent_plot.panel_boundaries.dragging||this.parent_plot.interaction.dragging,!!t},n.Dashboard.prototype.show=function(){if(!this.selector){switch(this.type){case"plot":this.selector=t.select(this.parent.svg.node().parentNode).insert("div",":first-child");break;case"panel":this.selector=t.select(this.parent.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip, .lz-dashboard-menu, .lz-curtain").classed("lz-panel-dashboard",!0); -}this.selector.classed("lz-dashboard",!0).classed("lz-"+this.type+"-dashboard",!0).attr("id",this.id)}return this.components.forEach(function(t){t.show()}),this.selector.style({visibility:"visible"}),this.update()},n.Dashboard.prototype.update=function(){return this.selector?(this.components.forEach(function(t){t.update()}),this.position()):this},n.Dashboard.prototype.position=function(){if(!this.selector)return this;if("panel"===this.type){var t=this.parent.getPageOrigin(),e=(t.y+3.5).toString()+"px",a=t.x.toString()+"px",i=(this.parent.layout.width-4).toString()+"px";this.selector.style({position:"absolute",top:e,left:a,width:i})}return this.components.forEach(function(t){t.position()}),this},n.Dashboard.prototype.hide=function(){return!this.selector||this.shouldPersist()?this:(this.components.forEach(function(t){t.hide()}),this.selector.style({visibility:"hidden"}),this)},n.Dashboard.prototype.destroy=function(t){return"undefined"==typeof t&&(t=!1),this.selector?this.shouldPersist()&&!t?this:(this.components.forEach(function(t){t.destroy(!0)}),this.components=[],this.selector.remove(),this.selector=null,this):this},n.Dashboard.Component=function(t,e){return this.layout=t||{},this.layout.color||(this.layout.color="gray"),this.parent=e||null,this.parent_panel=null,this.parent_plot=null,this.parent_svg=null,this.parent instanceof n.Dashboard&&("panel"===this.parent.type?(this.parent_panel=this.parent.parent,this.parent_plot=this.parent.parent.parent,this.parent_svg=this.parent_panel):(this.parent_plot=this.parent.parent,this.parent_svg=this.parent_plot)),this.selector=null,this.button=null,this.persist=!1,this.layout.position||(this.layout.position="left"),this},n.Dashboard.Component.prototype.show=function(){if(this.parent&&this.parent.selector){if(!this.selector){var t=["start","middle","end"].indexOf(this.layout.group_position)!==-1?" lz-dashboard-group-"+this.layout.group_position:"";this.selector=this.parent.selector.append("div").attr("class","lz-dashboard-"+this.layout.position+t),this.layout.style&&this.selector.style(this.layout.style),"function"==typeof this.initialize&&this.initialize()}return this.button&&"highlighted"===this.button.status&&this.button.menu.show(),this.selector.style({visibility:"visible"}),this.update(),this.position()}},n.Dashboard.Component.prototype.update=function(){},n.Dashboard.Component.prototype.position=function(){return this.button&&this.button.menu.position(),this},n.Dashboard.Component.prototype.shouldPersist=function(){return!!this.persist||!(!this.button||!this.button.persist)},n.Dashboard.Component.prototype.hide=function(){return!this.selector||this.shouldPersist()?this:(this.button&&this.button.menu.hide(),this.selector.style({visibility:"hidden"}),this)},n.Dashboard.Component.prototype.destroy=function(t){return"undefined"==typeof t&&(t=!1),this.selector?this.shouldPersist()&&!t?this:(this.button&&this.button.menu&&this.button.menu.destroy(),this.selector.remove(),this.selector=null,this.button=null,this):this},n.Dashboard.Components=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t]){if("object"!=typeof a)throw"invalid layout argument for dashboard component ["+t+"]";return new e[t](a,i)}throw"dashboard component ["+t+"] not found"}return null},t.set=function(t,a){if(a){if("function"!=typeof a)throw"unable to set dashboard component ["+t+"], argument provided is not a function";e[t]=a,e[t].prototype=new n.Dashboard.Component}else delete e[t]},t.add=function(a,i){if(e[a])throw"dashboard component already exists with name: "+a;t.set(a,i)},t.list=function(){return Object.keys(e)},t}(),n.Dashboard.Component.Button=function(e){if(!(e instanceof n.Dashboard.Component))throw"Unable to create dashboard component button, invalid parent";this.parent=e,this.parent_panel=this.parent.parent_panel,this.parent_plot=this.parent.parent_plot,this.parent_svg=this.parent.parent_svg,this.parent_dashboard=this.parent.parent,this.selector=null,this.tag="a",this.setTag=function(t){return"undefined"!=typeof t&&(this.tag=t.toString()),this},this.html="",this.setHtml=function(t){return"undefined"!=typeof t&&(this.html=t.toString()),this},this.setText=this.setHTML,this.title="",this.setTitle=function(t){return"undefined"!=typeof t&&(this.title=t.toString()),this},this.color="gray",this.setColor=function(t){return"undefined"!=typeof t&&(["gray","red","orange","yellow","green","blue","purple"].indexOf(t)!==-1?this.color=t:this.color="gray"),this},this.style={},this.setStyle=function(t){return"undefined"!=typeof t&&(this.style=t),this},this.getClass=function(){var t=["start","middle","end"].indexOf(this.parent.layout.group_position)!==-1?" lz-dashboard-button-group-"+this.parent.layout.group_position:"";return"lz-dashboard-button lz-dashboard-button-"+this.color+(this.status?"-"+this.status:"")+t},this.persist=!1,this.permanent=!1,this.setPermanent=function(t){return t="undefined"==typeof t||Boolean(t),this.permanent=t,this.permanent&&(this.persist=!0),this},this.shouldPersist=function(){return this.permanent||this.persist},this.status="",this.setStatus=function(t){return"undefined"!=typeof t&&["","highlighted","disabled"].indexOf(t)!==-1&&(this.status=t),this.update()},this.highlight=function(t){return t="undefined"==typeof t||Boolean(t),t?this.setStatus("highlighted"):"highlighted"===this.status?this.setStatus(""):this},this.disable=function(t){return t="undefined"==typeof t||Boolean(t),t?this.setStatus("disabled"):"disabled"===this.status?this.setStatus(""):this},this.onmouseover=function(){},this.setOnMouseover=function(t){return"function"==typeof t?this.onmouseover=t:this.onmouseover=function(){},this},this.onmouseout=function(){},this.setOnMouseout=function(t){return"function"==typeof t?this.onmouseout=t:this.onmouseout=function(){},this},this.onclick=function(){},this.setOnclick=function(t){return"function"==typeof t?this.onclick=t:this.onclick=function(){},this},this.show=function(){if(this.parent)return this.selector||(this.selector=this.parent.selector.append(this.tag).attr("class",this.getClass())),this.update()},this.preUpdate=function(){return this},this.update=function(){return this.selector?(this.preUpdate(),this.selector.attr("class",this.getClass()).attr("title",this.title).style(this.style).on("mouseover","disabled"===this.status?null:this.onmouseover).on("mouseout","disabled"===this.status?null:this.onmouseout).on("click","disabled"===this.status?null:this.onclick).html(this.html),this.menu.update(),this.postUpdate(),this):this},this.postUpdate=function(){return this},this.hide=function(){return this.selector&&!this.shouldPersist()&&(this.selector.remove(),this.selector=null),this},this.menu={outer_selector:null,inner_selector:null,scroll_position:0,hidden:!0,show:function(){return this.menu.outer_selector||(this.menu.outer_selector=t.select(this.parent_plot.svg.node().parentNode).append("div").attr("class","lz-dashboard-menu lz-dashboard-menu-"+this.color).attr("id",this.parent_svg.getBaseId()+".dashboard.menu"),this.menu.inner_selector=this.menu.outer_selector.append("div").attr("class","lz-dashboard-menu-content"),this.menu.inner_selector.on("scroll",function(){this.menu.scroll_position=this.menu.inner_selector.node().scrollTop}.bind(this))),this.menu.outer_selector.style({visibility:"visible"}),this.menu.hidden=!1,this.menu.update()}.bind(this),update:function(){return this.menu.outer_selector?(this.menu.populate(),this.menu.inner_selector&&(this.menu.inner_selector.node().scrollTop=this.menu.scroll_position),this.menu.position()):this.menu}.bind(this),position:function(){if(!this.menu.outer_selector)return this.menu;this.menu.outer_selector.style({height:null});var t=3,e=20,a=14,i=this.parent_svg.getPageOrigin(),n=document.documentElement.scrollTop||document.body.scrollTop,s=this.parent_plot.getContainerOffset(),o=this.parent_dashboard.selector.node().getBoundingClientRect(),r=this.selector.node().getBoundingClientRect(),l=this.menu.outer_selector.node().getBoundingClientRect(),h=this.menu.inner_selector.node().scrollHeight,d=0,u=0;"panel"===this.parent_dashboard.type?(d=i.y+o.height+2*t,u=Math.max(i.x+this.parent_svg.layout.width-l.width-t,i.x+t)):(d=r.bottom+n+t-s.top,u=Math.max(r.left+r.width-l.width-s.left,i.x+t));var p=Math.max(this.parent_svg.layout.width-2*t-e,e),c=p,y=p-4*t,f=Math.max(this.parent_svg.layout.height-10*t-a,a),g=Math.min(h,f),_=f;return this.menu.outer_selector.style({top:d.toString()+"px",left:u.toString()+"px","max-width":c.toString()+"px","max-height":_.toString()+"px",height:g.toString()+"px"}),this.menu.inner_selector.style({"max-width":y.toString()+"px"}),this.menu.inner_selector.node().scrollTop=this.menu.scroll_position,this.menu}.bind(this),hide:function(){return this.menu.outer_selector?(this.menu.outer_selector.style({visibility:"hidden"}),this.menu.hidden=!0,this.menu):this.menu}.bind(this),destroy:function(){return this.menu.outer_selector?(this.menu.inner_selector.remove(),this.menu.outer_selector.remove(),this.menu.inner_selector=null,this.menu.outer_selector=null,this.menu):this.menu}.bind(this),populate:function(){}.bind(this),setPopulate:function(t){return"function"==typeof t?(this.menu.populate=t,this.setOnclick(function(){this.menu.hidden?(this.menu.show(),this.highlight().update(),this.persist=!0):(this.menu.hide(),this.highlight(!1).update(),this.permanent||(this.persist=!1))}.bind(this))):this.setOnclick(),this}.bind(this)}},n.Dashboard.Components.add("title",function(t){n.Dashboard.Component.apply(this,arguments),this.show=function(){return this.div_selector=this.parent.selector.append("div").attr("class","lz-dashboard-title lz-dashboard-"+this.layout.position),this.title_selector=this.div_selector.append("h3"),this.update()},this.update=function(){var e=t.title.toString();return this.layout.subtitle&&(e+=" "+this.layout.subtitle+""),this.title_selector.html(e),this}}),n.Dashboard.Components.add("dimensions",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){var e=this.parent_plot.layout.width.toString().indexOf(".")===-1?this.parent_plot.layout.width:this.parent_plot.layout.width.toFixed(2),a=this.parent_plot.layout.height.toString().indexOf(".")===-1?this.parent_plot.layout.height:this.parent_plot.layout.height.toFixed(2);return this.selector.html(e+"px × "+a+"px"),t.class&&this.selector.attr("class",t.class),t.style&&this.selector.style(t.style),this}}),n.Dashboard.Components.add("region_scale",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)||null===this.parent_plot.state.start||null===this.parent_plot.state.end?this.selector.style("display","none"):(this.selector.style("display",null),this.selector.html(n.positionIntToString(this.parent_plot.state.end-this.parent_plot.state.start,null,!0))),t.class&&this.selector.attr("class",t.class),t.style&&this.selector.style(t.style),this}}),n.Dashboard.Components.add("download",function(a){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(a.color).setHtml("Download Image").setTitle("Download image of the current plot as locuszoom.svg").setOnMouseover(function(){this.button.selector.classed("lz-dashboard-button-gray-disabled",!0).html("Preparing Image"),this.generateBase64SVG().then(function(t){this.button.selector.attr("href","data:image/svg+xml;base64,\n"+t).classed("lz-dashboard-button-gray-disabled",!1).classed("lz-dashboard-button-gray-highlighted",!0).html("Download Image")}.bind(this))}.bind(this)).setOnMouseout(function(){this.button.selector.classed("lz-dashboard-button-gray-highlighted",!1)}.bind(this)),this.button.show(),this.button.selector.attr("href-lang","image/svg+xml").attr("download","locuszoom.svg"),this)},this.css_string="";for(var i in Object.keys(document.styleSheets))if(null!==document.styleSheets[i].href&&document.styleSheets[i].href.indexOf("locuszoom.css")!==-1){n.createCORSPromise("GET",document.styleSheets[i].href).then(function(t){this.css_string=t.replace(/[\r\n]/g," ").replace(/\s+/g," "),this.css_string.indexOf("/* ! LocusZoom HTML Styles */")&&(this.css_string=this.css_string.substring(0,this.css_string.indexOf("/* ! LocusZoom HTML Styles */")))}.bind(this));break}this.generateBase64SVG=function(){return e.fcall(function(){var e=this.parent.selector.append("div").style("display","none").html(this.parent_plot.svg.node().outerHTML);e.selectAll("g.lz-curtain").remove(),e.selectAll("g.lz-mouse_guide").remove(),e.selectAll("g.tick text").each(function(){var e=10*+t.select(this).attr("dy").substring(-2).slice(0,-2);t.select(this).attr("dy",e)});var a=t.select(e.select("svg").node().parentNode).html(),i='",n=a.indexOf(">")+1;return a=a.slice(0,n)+i+a.slice(n),e.remove(),btoa(encodeURIComponent(a).replace(/%([0-9A-F]{2})/g,function(t,e){return String.fromCharCode("0x"+e)}))}.bind(this))}}),n.Dashboard.Components.add("remove_panel",function(e){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(e.color).setHtml("×").setTitle("Remove panel").setOnclick(function(){if(!e.suppress_confirm&&!confirm("Are you sure you want to remove this panel? This cannot be undone!"))return!1;var a=this.parent_panel;return a.dashboard.hide(!0),t.select(a.parent.svg.node().parentNode).on("mouseover."+a.getBaseId()+".dashboard",null),t.select(a.parent.svg.node().parentNode).on("mouseout."+a.getBaseId()+".dashboard",null),a.parent.removePanel(a.id)}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("move_panel_up",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){if(this.button){var e=0===this.parent_panel.layout.y_index;return this.button.disable(e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("▴").setTitle("Move panel up").setOnclick(function(){this.parent_panel.moveUp(),this.update()}.bind(this)),this.button.show(),this.update()}}),n.Dashboard.Components.add("move_panel_down",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){if(this.button){var e=this.parent_panel.layout.y_index===this.parent_plot.panel_ids_by_y_index.length-1;return this.button.disable(e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("▾").setTitle("Move panel down").setOnclick(function(){this.parent_panel.moveDown(),this.update()}.bind(this)),this.button.show(),this.update()}}),n.Dashboard.Components.add("shift_region",function(t){return n.Dashboard.Component.apply(this,arguments),isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)?(this.update=function(){},void console.warn("Unable to add shift_region dashboard component: plot state does not have region bounds")):((isNaN(t.step)||0===t.step)&&(t.step=5e4),"string"!=typeof t.button_html&&(t.button_html=t.step>0?">":"<"),"string"!=typeof t.button_title&&(t.button_title="Shift region by "+(t.step>0?"+":"-")+n.positionIntToString(Math.abs(t.step),null,!0)),void(this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){this.parent_plot.applyState({start:Math.max(this.parent_plot.state.start+t.step,1),end:this.parent_plot.state.end+t.step})}.bind(this)),this.button.show(),this)}))}),n.Dashboard.Components.add("zoom_region",function(t){return n.Dashboard.Component.apply(this,arguments),isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)?(this.update=function(){},void console.warn("Unable to add zoom_region dashboard component: plot state does not have region bounds")):((isNaN(t.step)||0===t.step)&&(t.step=.2),"string"!=typeof t.button_html&&(t.button_html=t.step>0?"z–":"z+"),"string"!=typeof t.button_title&&(t.button_title="Zoom region "+(t.step>0?"out":"in")+" by "+(100*Math.abs(t.step)).toFixed(1)+"%"),void(this.update=function(){if(this.button){var e=!0,a=this.parent_plot.state.end-this.parent_plot.state.start;return t.step>0&&!isNaN(this.parent_plot.layout.max_region_scale)&&a>=this.parent_plot.layout.max_region_scale&&(e=!1),t.step<0&&!isNaN(this.parent_plot.layout.min_region_scale)&&a<=this.parent_plot.layout.min_region_scale&&(e=!1),this.button.disable(!e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){var e=this.parent_plot.state.end-this.parent_plot.state.start,a=1+t.step,i=e*a;isNaN(this.parent_plot.layout.max_region_scale)||(i=Math.min(i,this.parent_plot.layout.max_region_scale)),isNaN(this.parent_plot.layout.min_region_scale)||(i=Math.max(i,this.parent_plot.layout.min_region_scale));var n=Math.floor((i-e)/2);this.parent_plot.applyState({start:Math.max(this.parent_plot.state.start-n,1),end:this.parent_plot.state.end+n})}.bind(this)),this.button.show(),this}))}),n.Dashboard.Components.add("menu",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title),this.button.menu.setPopulate(function(){this.button.menu.inner_selector.html(t.menu_html)}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("covariates_model",function(t){n.Dashboard.Component.apply(this,arguments),this.initialize=function(){this.parent_plot.state.model=this.parent_plot.state.model||{},this.parent_plot.state.model.covariates=this.parent_plot.state.model.covariates||[],this.parent_plot.CovariatesModel={button:this,add:function(t){var e=JSON.parse(JSON.stringify(t));"object"==typeof t&&"string"!=typeof e.html&&(e.html="function"==typeof t.toHTML?t.toHTML():t.toString());for(var a=0;a1?"covariates":"covariate";t+=" ("+this.parent_plot.state.model.covariates.length+" "+e+")"}this.button.setHtml(t).disable(!1)}.bind(this),this.button.show(),this)}}),n.Dashboard.Components.add("toggle_split_tracks",function(t){if(n.Dashboard.Component.apply(this,arguments),t.data_layer_id||(t.data_layer_id="intervals"),!this.parent_panel.data_layers[t.data_layer_id])throw"Dashboard toggle split tracks component missing valid data layer ID";this.update=function(){var e=this.parent_panel.data_layers[t.data_layer_id],a=e.layout.split_tracks?"Merge Tracks":"Split Tracks";return this.button?(this.button.setHtml(a),this.button.show(),this.parent.position(),this):(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(a).setTitle("Toggle whether tracks are split apart or merged together").setOnclick(function(){e.toggleSplitTracks(),this.scale_timeout&&clearTimeout(this.scale_timeout);var t=e.layout.transition?+e.layout.transition.duration||0:0;this.scale_timeout=setTimeout(function(){this.parent_panel.scaleHeightToData(),this.parent_plot.positionPanels()}.bind(this),t),this.update()}.bind(this)),this.update())}}),n.Dashboard.Components.add("resize_to_data",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("Resize to Data").setTitle("Automatically resize this panel to fit the data its currently showing").setOnclick(function(){this.parent_panel.scaleHeightToData(),this.update()}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("toggle_legend",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){var e=this.parent_panel.legend.layout.hidden?"Show Legend":"Hide Legend";return this.button?(this.button.setHtml(e).show(),this.parent.position(),this):(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setTitle("Show or hide the legend for this panel").setOnclick(function(){this.parent_panel.legend.layout.hidden=!this.parent_panel.legend.layout.hidden,this.parent_panel.legend.render(),this.update()}.bind(this)),this.update())}}),n.Dashboard.Components.add("data_layers",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return"string"!=typeof t.button_html&&(t.button_html="Data Layers"),"string"!=typeof t.button_title&&(t.button_title="Manipulate Data Layers (sort, dim, show/hide, etc.)"),this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){this.button.menu.populate()}.bind(this)),this.button.menu.setPopulate(function(){this.button.menu.inner_selector.html("");var e=this.button.menu.inner_selector.append("table");return this.parent_panel.data_layer_ids_by_z_index.slice().reverse().forEach(function(a,i){var s=this.parent_panel.data_layers[a],o="string"!=typeof s.layout.name?s.id:s.layout.name,r=e.append("tr");r.append("td").html(o),t.statuses.forEach(function(t){var e,a,i,o=n.DataLayer.Statuses.adjectives.indexOf(t),l=n.DataLayer.Statuses.verbs[o];s.global_statuses[t]?(e=n.DataLayer.Statuses.menu_antiverbs[o],a="un"+l+"AllElements",i="-highlighted"):(e=n.DataLayer.Statuses.verbs[o],a=l+"AllElements",i=""),r.append("td").append("a").attr("class","lz-dashboard-button lz-dashboard-button-"+this.layout.color+i).style({"margin-left":"0em"}).on("click",function(){s[a](),this.button.menu.populate()}.bind(this)).html(e)}.bind(this));var l=0===i,h=i===this.parent_panel.data_layer_ids_by_z_index.length-1,d=r.append("td");d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-start lz-dashboard-button-"+this.layout.color+(h?"-disabled":"")).style({"margin-left":"0em"}).on("click",function(){s.moveDown(),this.button.menu.populate()}.bind(this)).html("▾").attr("title","Move layer down (further back)"),d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-middle lz-dashboard-button-"+this.layout.color+(l?"-disabled":"")).style({"margin-left":"0em"}).on("click",function(){s.moveUp(),this.button.menu.populate()}.bind(this)).html("▴").attr("title","Move layer up (further front)"),d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-end lz-dashboard-button-red").style({"margin-left":"0em"}).on("click",function(){return confirm("Are you sure you want to remove the "+o+" layer? This cannot be undone!")&&s.parent.removeDataLayer(a),this.button.menu.populate()}.bind(this)).html("×").attr("title","Remove layer")}.bind(this)),this}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("display_options",function(t){"string"!=typeof t.button_html&&(t.button_html="Display options"),"string"!=typeof t.button_title&&(t.button_title="Control how plot items are displayed"),n.Dashboard.Component.apply(this,arguments);var e=t.fields_whitelist||["color","fill_opacity","label","legend","point_shape","point_size","tooltip","tooltip_positioning"],a=this.parent_panel.data_layers[t.layer_name],i=a.layout,s={};e.forEach(function(t){var e=i[t];e&&(s[t]=JSON.parse(JSON.stringify(e)))}),this._selected_item="default";var o=this;this.button=new n.Dashboard.Component.Button(o).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){o.button.menu.populate()}),this.button.menu.setPopulate(function(){var t=Math.floor(1e4*Math.random()).toString();o.button.menu.inner_selector.html("");var e=o.button.menu.inner_selector.append("table"),i=o.layout,n=function(i,n,s){var r=e.append("tr");r.append("td").append("input").attr({type:"radio",name:"color-picker-"+t,value:s}).property("checked",s===o._selected_item).on("click",function(){Object.keys(n).forEach(function(t){a.layout[t]=n[t]}),o._selected_item=s,o.parent_panel.render();var t=o.parent_panel.legend;t&&n.legend&&t.render()}),r.append("td").text(i)},r=i.default_config_display_name||"Default style";return n(r,s,"default"),i.options.forEach(function(t,e){n(t.display_name,t.display,e)}),o}),this.update=function(){return this.button.show(),this}}),n.Legend=function(t){if(!(t instanceof n.Panel))throw"Unable to create legend, parent must be a locuszoom panel";return this.parent=t,this.id=this.parent.getBaseId()+".legend",this.parent.layout.legend=n.Layouts.merge(this.parent.layout.legend||{},n.Legend.DefaultLayout),this.layout=this.parent.layout.legend,this.selector=null,this.background_rect=null,this.elements=[],this.elements_group=null,this.hidden=!1,this.render()},n.Legend.DefaultLayout={orientation:"vertical",origin:{x:0,y:0},width:10,height:10,padding:5,label_size:12,hidden:!1},n.Legend.prototype.render=function(){this.selector||(this.selector=this.parent.svg.group.append("g").attr("id",this.parent.getBaseId()+".legend").attr("class","lz-legend")),this.background_rect||(this.background_rect=this.selector.append("rect").attr("width",100).attr("height",100).attr("class","lz-legend-background")),this.elements_group||(this.elements_group=this.selector.append("g")),this.elements.forEach(function(t){t.remove()}),this.elements=[];var e=+this.layout.padding||1,a=e,i=e,n=0;this.parent.data_layer_ids_by_z_index.slice().reverse().forEach(function(s){Array.isArray(this.parent.data_layers[s].layout.legend)&&this.parent.data_layers[s].layout.legend.forEach(function(s){var o=this.elements_group.append("g").attr("transform","translate("+a+","+i+")"),r=+s.label_size||+this.layout.label_size||12,l=0,h=r/2+e/2;if(n=Math.max(n,r+e),"line"===s.shape){var d=+s.length||16,u=r/4+e/2;o.append("path").attr("class",s.class||"").attr("d","M0,"+u+"L"+d+","+u).style(s.style||{}),l=d+e}else if("rect"===s.shape){var p=+s.width||16,c=+s.height||p;o.append("rect").attr("class",s.class||"").attr("width",p).attr("height",c).attr("fill",s.color||{}).style(s.style||{}),l=p+e,n=Math.max(n,c+e)}else if(t.svg.symbolTypes.indexOf(s.shape)!==-1){var y=+s.size||40,f=Math.ceil(Math.sqrt(y/Math.PI));o.append("path").attr("class",s.class||"").attr("d",t.svg.symbol().size(y).type(s.shape)).attr("transform","translate("+f+","+(f+e/2)+")").attr("fill",s.color||{}).style(s.style||{}),l=2*f+e,h=Math.max(2*f+e/2,h),n=Math.max(n,2*f+e)}o.append("text").attr("text-anchor","left").attr("class","lz-label").attr("x",l).attr("y",h).style({"font-size":r}).text(s.label);var g=o.node().getBoundingClientRect();if("vertical"===this.layout.orientation)i+=g.height+e,n=0;else{var _=this.layout.origin.x+a+g.width;a>e&&_>this.parent.layout.width&&(i+=n,a=e,o.attr("transform","translate("+a+","+i+")")),a+=g.width+3*e}this.elements.push(o)}.bind(this))}.bind(this));var s=this.elements_group.node().getBoundingClientRect();return this.layout.width=s.width+2*this.layout.padding,this.layout.height=s.height+2*this.layout.padding,this.background_rect.attr("width",this.layout.width).attr("height",this.layout.height),this.selector.style({visibility:this.layout.hidden?"hidden":"visible"}),this.position()},n.Legend.prototype.position=function(){if(!this.selector)return this;var t=this.selector.node().getBoundingClientRect();isNaN(+this.layout.pad_from_bottom)||(this.layout.origin.y=this.parent.layout.height-t.height-+this.layout.pad_from_bottom),isNaN(+this.layout.pad_from_right)||(this.layout.origin.x=this.parent.layout.width-t.width-+this.layout.pad_from_right),this.selector.attr("transform","translate("+this.layout.origin.x+","+this.layout.origin.y+")")},n.Legend.prototype.hide=function(){this.layout.hidden=!0,this.render()},n.Legend.prototype.show=function(){this.layout.hidden=!1,this.render()},n.Data=n.Data||{},n.DataSources=function(){this.sources={}},n.DataSources.prototype.addSource=function(t,e){return console.warn("Warning: .addSource() is deprecated. Use .add() instead"),this.add(t,e)},n.DataSources.prototype.add=function(t,e){return this.set(t,e)},n.DataSources.prototype.set=function(t,e){if(Array.isArray(e)){var a=n.KnownDataSources.create.apply(null,e);this.sources[t]=a}else null!==e?this.sources[t]=e:delete this.sources[t];return this},n.DataSources.prototype.getSource=function(t){return console.warn("Warning: .getSource() is deprecated. Use .get() instead"),this.get(t)},n.DataSources.prototype.get=function(t){return this.sources[t]},n.DataSources.prototype.removeSource=function(t){return console.warn("Warning: .removeSource() is deprecated. Use .remove() instead"),this.remove(t)},n.DataSources.prototype.remove=function(t){return this.set(t,null)},n.DataSources.prototype.fromJSON=function(t){"string"==typeof t&&(t=JSON.parse(t));var e=this;return Object.keys(t).forEach(function(a){e.set(a,t[a])}),e},n.DataSources.prototype.keys=function(){return Object.keys(this.sources)},n.DataSources.prototype.toJSON=function(){return this.sources},n.Data.Field=function(t){var e=/^(?:([^:]+):)?([^:|]*)(\|.+)*$/.exec(t);this.full_name=t,this.namespace=e[1]||null,this.name=e[2]||null,this.transformations=[],"string"==typeof e[3]&&e[3].length>1&&(this.transformations=e[3].substring(1).split("|"),this.transformations.forEach(function(t,e){this.transformations[e]=n.TransformationFunctions.get(t)}.bind(this))),this.applyTransformations=function(t){return this.transformations.forEach(function(e){t=e(t)}),t},this.resolve=function(t){if("undefined"==typeof t[this.full_name]){var e=null;"undefined"!=typeof t[this.namespace+":"+this.name]?e=t[this.namespace+":"+this.name]:"undefined"!=typeof t[this.name]&&(e=t[this.name]),t[this.full_name]=this.applyTransformations(e)}return t[this.full_name]}},n.Data.Requester=function(t){function a(t){var e={},a=/^(?:([^:]+):)?([^:|]*)(\|.+)*$/;return t.forEach(function(t){var i=a.exec(t),s=i[1]||"base",o=i[2],r=n.TransformationFunctions.get(i[3]);"undefined"==typeof e[s]&&(e[s]={outnames:[],fields:[],trans:[]}),e[s].outnames.push(t),e[s].fields.push(o),e[s].trans.push(r)}),e}this.getData=function(i,n){ -for(var s=a(n),o=Object.keys(s).map(function(e){if(!t.get(e))throw"Datasource for namespace "+e+" not found";return t.get(e).getData(i,s[e].fields,s[e].outnames,s[e].trans)}),r=e.when({header:{},body:{}}),l=0;l1&&(2!==e.length||e.indexOf("isrefvar")===-1))throw"LD does not know how to get all fields: "+e.join(", ")},n.Data.LDSource.prototype.findMergeFields=function(t){var e=function(t){return function(){for(var e=arguments,a=0;a0){var i=Object.keys(t.body[0]),n=e(i);a.id=a.id||n(/\bvariant\b/)||n(/\bid\b/),a.position=a.position||n(/\bposition\b/i,/\bpos\b/i),a.pvalue=a.pvalue||n(/\bpvalue\b/i,/\blog_pvalue\b/i),a._names_=i}return a},n.Data.LDSource.prototype.findRequestedFields=function(t,e){for(var a={},i=0;ii&&(i=t[s][e]*a,n=s);return n},n=t.ldrefsource||e.header.ldrefsource||1,s=this.findRequestedFields(a),o=s.ldin;if("state"===o&&(o=t.ldrefvar||e.header.ldrefvar||"best"),"best"===o){if(!e.body)throw"No association data found to find best pvalue";var r=this.findMergeFields(e);if(!r.pvalue||!r.id){var l="";throw r.id||(l+=(l.length?", ":"")+"id"),r.pvalue||(l+=(l.length?", ":"")+"pvalue"),"Unable to find necessary column(s) for merge: "+l+" (available: "+r._names_+")"}o=e.body[i(e.body,r.pvalue)][r.id]}return e.header||(e.header={}),e.header.ldrefvar=o,this.url+"results/?filter=reference eq "+n+" and chromosome2 eq '"+t.chr+"' and position2 ge "+t.start+" and position2 le "+t.end+" and variant1 eq '"+o+"'&fields=chr,pos,rsquare"},n.Data.LDSource.prototype.parseResponse=function(t,e,a,i){var n=JSON.parse(t),s=this.findMergeFields(e),o=this.findRequestedFields(a,i);if(!s.position)throw"Unable to find position field for merge: "+s._names_;var r=function(t,e,a,i){for(var n=0,o=0;n0&&parseFloat(this.panels[i].layout.proportional_height)>0&&(s=Math.max(s,this.panels[i].layout.min_height/this.panels[i].layout.proportional_height));if(this.layout.min_width=Math.max(n,1),this.layout.min_height=Math.max(s,1),t.select(this.svg.node().parentNode).style({"min-width":this.layout.min_width+"px","min-height":this.layout.min_height+"px"}),!isNaN(e)&&e>=0&&!isNaN(a)&&a>=0){this.layout.width=Math.max(Math.round(+e),this.layout.min_width),this.layout.height=Math.max(Math.round(+a),this.layout.min_height),this.layout.aspect_ratio=this.layout.width/this.layout.height,this.layout.responsive_resize&&(this.svg&&(this.layout.width=Math.max(this.svg.node().parentNode.getBoundingClientRect().width,this.layout.min_width)),this.layout.height=this.layout.width/this.layout.aspect_ratio,this.layout.height0)e.layout.y_index<0&&(e.layout.y_index=Math.max(this.panel_ids_by_y_index.length+e.layout.y_index,0)),this.panel_ids_by_y_index.splice(e.layout.y_index,0,e.id),this.applyPanelYIndexesToPanelLayouts();else{var a=this.panel_ids_by_y_index.push(e.id);this.panels[e.id].layout.y_index=a-1}var i=null;return this.layout.panels.forEach(function(t,a){t.id===e.id&&(i=a)}),null===i&&(i=this.layout.panels.push(this.panels[e.id].layout)-1),this.panels[e.id].layout_idx=i,this.initialized&&(this.positionPanels(),this.panels[e.id].initialize(),this.panels[e.id].reMap(),this.setDimensions(this.layout.width,this.layout.height)),this.panels[e.id]},n.Plot.prototype.clearPanelData=function(t,e){e=e||"wipe";var a;a=t?[t]:Object.keys(this.panels);var i=this;return a.forEach(function(t){i.panels[t].data_layer_ids_by_z_index.forEach(function(a){var n=i.panels[t].data_layers[a];n.destroyAllTooltips(),delete i.layout.state[t+"."+a],"reset"===e&&n.setDefaultState()})}),this},n.Plot.prototype.removePanel=function(t){if(!this.panels[t])throw"Unable to remove panel, ID not found: "+t;return this.panel_boundaries.hide(),this.clearPanelData(t),this.panels[t].loader.hide(),this.panels[t].dashboard.destroy(!0),this.panels[t].curtain.hide(),this.panels[t].svg.container&&this.panels[t].svg.container.remove(),this.layout.panels.splice(this.panels[t].layout_idx,1),delete this.panels[t],delete this.layout.state[t],this.layout.panels.forEach(function(t,e){this.panels[t.id].layout_idx=e}.bind(this)),this.panel_ids_by_y_index.splice(this.panel_ids_by_y_index.indexOf(t),1),this.applyPanelYIndexesToPanelLayouts(),this.initialized&&(this.layout.min_height=this._base_layout.min_height,this.layout.min_width=this._base_layout.min_width,this.positionPanels(),this.setDimensions(this.layout.width,this.layout.height)),this},n.Plot.prototype.positionPanels=function(){var t,e={left:0,right:0};for(t in this.panels)null===this.panels[t].layout.proportional_height&&(this.panels[t].layout.proportional_height=this.panels[t].layout.height/this.layout.height),null===this.panels[t].layout.proportional_width&&(this.panels[t].layout.proportional_width=1),this.panels[t].layout.interaction.x_linked&&(e.left=Math.max(e.left,this.panels[t].layout.margin.left),e.right=Math.max(e.right,this.panels[t].layout.margin.right));var a=this.sumProportional("height");if(!a)return this;var i=1/a;for(t in this.panels)this.panels[t].layout.proportional_height*=i;var n=0;this.panel_ids_by_y_index.forEach(function(t){if(this.panels[t].setOrigin(0,n),this.panels[t].layout.proportional_origin.x=0,n+=this.panels[t].layout.height,this.panels[t].layout.interaction.x_linked){var a=Math.max(e.left-this.panels[t].layout.margin.left,0)+Math.max(e.right-this.panels[t].layout.margin.right,0);this.panels[t].layout.width+=a,this.panels[t].layout.margin.left=e.left,this.panels[t].layout.margin.right=e.right,this.panels[t].layout.cliparea.origin.x=e.left}}.bind(this));var s=n;return this.panel_ids_by_y_index.forEach(function(t){this.panels[t].layout.proportional_origin.y=this.panels[t].layout.origin.y/s}.bind(this)),this.setDimensions(),this.panel_ids_by_y_index.forEach(function(t){this.panels[t].setDimensions(this.layout.width*this.panels[t].layout.proportional_width,this.layout.height*this.panels[t].layout.proportional_height)}.bind(this)),this},n.Plot.prototype.initialize=function(){if(this.layout.responsive_resize&&t.select(this.container).classed("lz-container-responsive",!0),this.layout.mouse_guide){var e=this.svg.append("g").attr("class","lz-mouse_guide").attr("id",this.id+".mouse_guide"),a=e.append("rect").attr("class","lz-mouse_guide-vertical").attr("x",-1),i=e.append("rect").attr("class","lz-mouse_guide-horizontal").attr("y",-1);this.mouse_guide={svg:e,vertical:a,horizontal:i}}this.curtain=n.generateCurtain.call(this),this.loader=n.generateLoader.call(this),this.panel_boundaries={parent:this,hide_timeout:null,showing:!1,dragging:!1,selectors:[],corner_selector:null,show:function(){if(!this.showing&&!this.parent.curtain.showing){this.showing=!0,this.parent.panel_ids_by_y_index.forEach(function(e,a){var i=t.select(this.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip").attr("class","lz-panel-boundary").attr("title","Resize panel");i.append("span");var n=t.behavior.drag();n.on("dragstart",function(){this.dragging=!0}.bind(this)),n.on("dragend",function(){this.dragging=!1}.bind(this)),n.on("drag",function(){var e=this.parent.panels[this.parent.panel_ids_by_y_index[a]],i=e.layout.height;e.setDimensions(e.layout.width,e.layout.height+t.event.dy);var n=e.layout.height-i,s=this.parent.layout.height+n;this.parent.panel_ids_by_y_index.forEach(function(t,e){var i=this.parent.panels[this.parent.panel_ids_by_y_index[e]];i.layout.proportional_height=i.layout.height/s,e>a&&(i.setOrigin(i.layout.origin.x,i.layout.origin.y+n),i.dashboard.position())}.bind(this)),this.parent.positionPanels(),this.position()}.bind(this)),i.call(n),this.parent.panel_boundaries.selectors.push(i)}.bind(this));var e=t.select(this.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip").attr("class","lz-panel-corner-boundary").attr("title","Resize plot");e.append("span").attr("class","lz-panel-corner-boundary-outer"),e.append("span").attr("class","lz-panel-corner-boundary-inner");var a=t.behavior.drag();a.on("dragstart",function(){this.dragging=!0}.bind(this)),a.on("dragend",function(){this.dragging=!1}.bind(this)),a.on("drag",function(){this.setDimensions(this.layout.width+t.event.dx,this.layout.height+t.event.dy)}.bind(this.parent)),e.call(a),this.parent.panel_boundaries.corner_selector=e}return this.position()},position:function(){if(!this.showing)return this;var t=this.parent.getPageOrigin();this.selectors.forEach(function(e,a){var i=this.parent.panels[this.parent.panel_ids_by_y_index[a]].getPageOrigin(),n=t.x,s=i.y+this.parent.panels[this.parent.panel_ids_by_y_index[a]].layout.height-12,o=this.parent.layout.width-1;e.style({top:s+"px",left:n+"px",width:o+"px"}),e.select("span").style({width:o+"px"})}.bind(this));var e=10,a=16;return this.corner_selector.style({top:t.y+this.parent.layout.height-e-a+"px",left:t.x+this.parent.layout.width-e-a+"px"}),this},hide:function(){return this.showing?(this.showing=!1,this.selectors.forEach(function(t){t.remove()}),this.selectors=[],this.corner_selector.remove(),this.corner_selector=null,this):this}},this.layout.panel_boundaries&&(t.select(this.svg.node().parentNode).on("mouseover."+this.id+".panel_boundaries",function(){clearTimeout(this.panel_boundaries.hide_timeout),this.panel_boundaries.show()}.bind(this)),t.select(this.svg.node().parentNode).on("mouseout."+this.id+".panel_boundaries",function(){this.panel_boundaries.hide_timeout=setTimeout(function(){this.panel_boundaries.hide()}.bind(this),300)}.bind(this))),this.dashboard=new n.Dashboard(this).show();for(var s in this.panels)this.panels[s].initialize();var o="."+this.id;if(this.layout.mouse_guide){var r=function(){this.mouse_guide.vertical.attr("x",-1),this.mouse_guide.horizontal.attr("y",-1)}.bind(this),l=function(){var e=t.mouse(this.svg.node());this.mouse_guide.vertical.attr("x",e[0]),this.mouse_guide.horizontal.attr("y",e[1])}.bind(this);this.svg.on("mouseout"+o+"-mouse_guide",r).on("touchleave"+o+"-mouse_guide",r).on("mousemove"+o+"-mouse_guide",l)}var h=function(){this.stopDrag()}.bind(this),d=function(){if(this.interaction.dragging){var e=t.mouse(this.svg.node());t.event&&t.event.preventDefault(),this.interaction.dragging.dragged_x=e[0]-this.interaction.dragging.start_x,this.interaction.dragging.dragged_y=e[1]-this.interaction.dragging.start_y,this.panels[this.interaction.panel_id].render(),this.interaction.linked_panel_ids.forEach(function(t){this.panels[t].render()}.bind(this))}}.bind(this);this.svg.on("mouseup"+o,h).on("touchend"+o,h).on("mousemove"+o,d).on("touchmove"+o,d),t.select("body").empty()||t.select("body").on("mouseup"+o,h).on("touchend"+o,h),this.initialized=!0;var u=this.svg.node().getBoundingClientRect(),p=u.width?u.width:this.layout.width,c=u.height?u.height:this.layout.height;return this.setDimensions(p,c),this},n.Plot.prototype.refresh=function(){return this.applyState()},n.Plot.prototype.applyState=function(t){if(t=t||{},"object"!=typeof t)throw"LocusZoom.applyState only accepts an object; "+typeof t+" given";var a=JSON.parse(JSON.stringify(this.state));for(var i in t)a[i]=t[i];a=n.validateState(a,this.layout);for(i in a)this.state[i]=a[i];this.emit("data_requested"),this.remap_promises=[],this.loading_data=!0;for(var s in this.panels)this.remap_promises.push(this.panels[s].reMap());return e.all(this.remap_promises).catch(function(t){console.error(t),this.curtain.drop(t),this.loading_data=!1}.bind(this)).then(function(){this.dashboard.update(),this.panel_ids_by_y_index.forEach(function(t){var e=this.panels[t];e.dashboard.update(),e.data_layer_ids_by_z_index.forEach(function(e){var a=this.data_layers[e],i=t+"."+e;for(var n in this.state[i])this.state[i].hasOwnProperty(n)&&Array.isArray(this.state[i][n])&&this.state[i][n].forEach(function(t){try{this.setElementStatus(n,this.getElementById(t),!0)}catch(t){console.error("Unable to apply state: "+i+", "+n)}}.bind(a))}.bind(e))}.bind(this)),this.emit("layout_changed"),this.emit("data_rendered"),this.loading_data=!1}.bind(this))},n.Plot.prototype.startDrag=function(e,a){e=e||null,a=a||null;var i=null;switch(a){case"background":case"x_tick":i="x";break;case"y1_tick":i="y1";break;case"y2_tick":i="y2"}if(!(e instanceof n.Panel&&i&&this.canInteract()))return this.stopDrag();var s=t.mouse(this.svg.node());return this.interaction={panel_id:e.id,linked_panel_ids:e.getLinkedPanelIds(i),dragging:{method:a,start_x:s[0],start_y:s[1],dragged_x:0,dragged_y:0,axis:i}},this.svg.style("cursor","all-scroll"),this},n.Plot.prototype.stopDrag=function(){if(!this.interaction.dragging)return this;if("object"!=typeof this.panels[this.interaction.panel_id])return this.interaction={},this;var t=this.panels[this.interaction.panel_id],e=function(e,a,i){t.data_layer_ids_by_z_index.forEach(function(n){t.data_layers[n].layout[e+"_axis"].axis===a&&(t.data_layers[n].layout[e+"_axis"].floor=i[0],t.data_layers[n].layout[e+"_axis"].ceiling=i[1],delete t.data_layers[n].layout[e+"_axis"].lower_buffer,delete t.data_layers[n].layout[e+"_axis"].upper_buffer,delete t.data_layers[n].layout[e+"_axis"].min_extent,delete t.data_layers[n].layout[e+"_axis"].ticks)})};switch(this.interaction.dragging.method){case"background":case"x_tick":0!==this.interaction.dragging.dragged_x&&(e("x",1,t.x_extent),this.applyState({start:t.x_extent[0],end:t.x_extent[1]}));break;case"y1_tick":case"y2_tick":if(0!==this.interaction.dragging.dragged_y){var a=parseInt(this.interaction.dragging.method[1]);e("y",a,t["y"+a+"_extent"])}}return this.interaction={},this.svg.style("cursor",null),this},n.Panel=function(t,e){if("object"!=typeof t)throw"Unable to create panel, invalid layout";if(this.parent=e||null,this.parent_plot=e,"string"==typeof t.id&&t.id.length){if(this.parent&&"undefined"!=typeof this.parent.panels[t.id])throw"Cannot create panel with id ["+t.id+"]; panel with that id already exists"}else if(this.parent){var a=null,i=function(){a="p"+Math.floor(Math.random()*Math.pow(10,8)),null!=a&&"undefined"==typeof this.parent.panels[a]||(a=i())}.bind(this);t.id=a}else t.id="p"+Math.floor(Math.random()*Math.pow(10,8));return this.id=t.id,this.initialized=!1,this.layout_idx=null,this.svg={},this.layout=n.Layouts.merge(t||{},n.Panel.DefaultLayout),this.parent?(this.state=this.parent.state,this.state_id=this.id,this.state[this.state_id]=this.state[this.state_id]||{}):(this.state=null,this.state_id=null),this.data_layers={},this.data_layer_ids_by_z_index=[],this.applyDataLayerZIndexesToDataLayerLayouts=function(){this.data_layer_ids_by_z_index.forEach(function(t,e){this.data_layers[t].layout.z_index=e}.bind(this))}.bind(this),this.data_promises=[],this.x_scale=null,this.y1_scale=null,this.y2_scale=null,this.x_extent=null,this.y1_extent=null,this.y2_extent=null,this.x_ticks=[],this.y1_ticks=[],this.y2_ticks=[],this.zoom_timeout=null,this.getBaseId=function(){return this.parent.id+"."+this.id},this.event_hooks={layout_changed:[],data_requested:[],data_rendered:[],element_clicked:[]},this.on=function(t,e){if(!Array.isArray(this.event_hooks[t]))throw"Unable to register event hook, invalid event: "+t.toString();if("function"!=typeof e)throw"Unable to register event hook, invalid hook function passed";return this.event_hooks[t].push(e),this},this.emit=function(t,e){if(!Array.isArray(this.event_hooks[t]))throw"LocusZoom attempted to throw an invalid event: "+t.toString();return e=e||this,this.event_hooks[t].forEach(function(t){t.call(e)}),this},this.getPageOrigin=function(){var t=this.parent.getPageOrigin();return{x:t.x+this.layout.origin.x,y:t.y+this.layout.origin.y}},this.initializeLayout(),this},n.Panel.DefaultLayout={title:{text:"",style:{},x:10,y:22},y_index:null,width:0,height:0,origin:{x:0,y:null},min_width:1,min_height:1,proportional_width:null,proportional_height:null,proportional_origin:{x:0,y:null},margin:{top:0,right:0,bottom:0,left:0},background_click:"clear_selections",dashboard:{components:[]},cliparea:{height:0,width:0,origin:{x:0,y:0}},axes:{x:{},y1:{},y2:{}},legend:null,interaction:{drag_background_to_pan:!1,drag_x_ticks_to_scale:!1,drag_y1_ticks_to_scale:!1,drag_y2_ticks_to_scale:!1,scroll_to_zoom:!1,x_linked:!1,y1_linked:!1,y2_linked:!1},data_layers:[]},n.Panel.prototype.initializeLayout=function(){if(0===this.layout.width&&null===this.layout.proportional_width&&(this.layout.proportional_width=1),0===this.layout.height&&null===this.layout.proportional_height){var t=Object.keys(this.parent.panels).length;t>0?this.layout.proportional_height=1/t:this.layout.proportional_height=1}return this.setDimensions(),this.setOrigin(),this.setMargin(),this.x_range=[0,this.layout.cliparea.width],this.y1_range=[this.layout.cliparea.height,0],this.y2_range=[this.layout.cliparea.height,0],["x","y1","y2"].forEach(function(t){Object.keys(this.layout.axes[t]).length&&this.layout.axes[t].render!==!1?(this.layout.axes[t].render=!0,this.layout.axes[t].label=this.layout.axes[t].label||null,this.layout.axes[t].label_function=this.layout.axes[t].label_function||null):this.layout.axes[t].render=!1}.bind(this)),this.layout.data_layers.forEach(function(t){this.addDataLayer(t)}.bind(this)),this},n.Panel.prototype.setDimensions=function(t,e){return"undefined"!=typeof t&&"undefined"!=typeof e?!isNaN(t)&&t>=0&&!isNaN(e)&&e>=0&&(this.layout.width=Math.max(Math.round(+t),this.layout.min_width),this.layout.height=Math.max(Math.round(+e),this.layout.min_height)):(null!==this.layout.proportional_width&&(this.layout.width=Math.max(this.layout.proportional_width*this.parent.layout.width,this.layout.min_width)),null!==this.layout.proportional_height&&(this.layout.height=Math.max(this.layout.proportional_height*this.parent.layout.height,this.layout.min_height))),this.layout.cliparea.width=Math.max(this.layout.width-(this.layout.margin.left+this.layout.margin.right),0),this.layout.cliparea.height=Math.max(this.layout.height-(this.layout.margin.top+this.layout.margin.bottom),0),this.svg.clipRect&&this.svg.clipRect.attr("width",this.layout.width).attr("height",this.layout.height),this.initialized&&(this.render(),this.curtain.update(),this.loader.update(),this.dashboard.update(),this.legend&&this.legend.position()),this},n.Panel.prototype.setOrigin=function(t,e){return!isNaN(t)&&t>=0&&(this.layout.origin.x=Math.max(Math.round(+t),0)),!isNaN(e)&&e>=0&&(this.layout.origin.y=Math.max(Math.round(+e),0)),this.initialized&&this.render(),this},n.Panel.prototype.setMargin=function(t,e,a,i){var n;return!isNaN(t)&&t>=0&&(this.layout.margin.top=Math.max(Math.round(+t),0)),!isNaN(e)&&e>=0&&(this.layout.margin.right=Math.max(Math.round(+e),0)),!isNaN(a)&&a>=0&&(this.layout.margin.bottom=Math.max(Math.round(+a),0)),!isNaN(i)&&i>=0&&(this.layout.margin.left=Math.max(Math.round(+i),0)),this.layout.margin.top+this.layout.margin.bottom>this.layout.height&&(n=Math.floor((this.layout.margin.top+this.layout.margin.bottom-this.layout.height)/2),this.layout.margin.top-=n,this.layout.margin.bottom-=n),this.layout.margin.left+this.layout.margin.right>this.layout.width&&(n=Math.floor((this.layout.margin.left+this.layout.margin.right-this.layout.width)/2),this.layout.margin.left-=n,this.layout.margin.right-=n),["top","right","bottom","left"].forEach(function(t){this.layout.margin[t]=Math.max(this.layout.margin[t],0)}.bind(this)),this.layout.cliparea.width=Math.max(this.layout.width-(this.layout.margin.left+this.layout.margin.right),0), -this.layout.cliparea.height=Math.max(this.layout.height-(this.layout.margin.top+this.layout.margin.bottom),0),this.layout.cliparea.origin.x=this.layout.margin.left,this.layout.cliparea.origin.y=this.layout.margin.top,this.initialized&&this.render(),this},n.Panel.prototype.setTitle=function(t){if("string"==typeof this.layout.title){var e=this.layout.title;this.layout.title={text:e,x:0,y:0,style:{}}}return"string"==typeof t?this.layout.title.text=t:"object"==typeof t&&null!==t&&(this.layout.title=n.Layouts.merge(t,this.layout.title)),this.layout.title.text.length?this.title.attr("display",null).attr("x",parseFloat(this.layout.title.x)).attr("y",parseFloat(this.layout.title.y)).style(this.layout.title.style).text(this.layout.title.text):this.title.attr("display","none"),this},n.Panel.prototype.initialize=function(){this.svg.container=this.parent.svg.append("g").attr("id",this.getBaseId()+".panel_container").attr("transform","translate("+(this.layout.origin.x||0)+","+(this.layout.origin.y||0)+")");var t=this.svg.container.append("clipPath").attr("id",this.getBaseId()+".clip");if(this.svg.clipRect=t.append("rect").attr("width",this.layout.width).attr("height",this.layout.height),this.svg.group=this.svg.container.append("g").attr("id",this.getBaseId()+".panel").attr("clip-path","url(#"+this.getBaseId()+".clip)"),this.curtain=n.generateCurtain.call(this),this.loader=n.generateLoader.call(this),this.dashboard=new n.Dashboard(this),this.inner_border=this.svg.group.append("rect").attr("class","lz-panel-background").on("click",function(){"clear_selections"===this.layout.background_click&&this.clearSelections()}.bind(this)),this.title=this.svg.group.append("text").attr("class","lz-panel-title"),"undefined"!=typeof this.layout.title&&this.setTitle(),this.svg.x_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".x_axis").attr("class","lz-x lz-axis"),this.layout.axes.x.render&&(this.svg.x_axis_label=this.svg.x_axis.append("text").attr("class","lz-x lz-axis lz-label").attr("text-anchor","middle")),this.svg.y1_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".y1_axis").attr("class","lz-y lz-y1 lz-axis"),this.layout.axes.y1.render&&(this.svg.y1_axis_label=this.svg.y1_axis.append("text").attr("class","lz-y1 lz-axis lz-label").attr("text-anchor","middle")),this.svg.y2_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".y2_axis").attr("class","lz-y lz-y2 lz-axis"),this.layout.axes.y2.render&&(this.svg.y2_axis_label=this.svg.y2_axis.append("text").attr("class","lz-y2 lz-axis lz-label").attr("text-anchor","middle")),this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].initialize()}.bind(this)),this.legend=null,this.layout.legend&&(this.legend=new n.Legend(this)),this.layout.interaction.drag_background_to_pan){var e="."+this.parent.id+"."+this.id+".interaction.drag",a=function(){this.parent.startDrag(this,"background")}.bind(this);this.svg.container.select(".lz-panel-background").on("mousedown"+e+".background",a).on("touchstart"+e+".background",a)}return this},n.Panel.prototype.resortDataLayers=function(){var e=[];this.data_layer_ids_by_z_index.forEach(function(t){e.push(this.data_layers[t].layout.z_index)}.bind(this)),this.svg.group.selectAll("g.lz-data_layer-container").data(e).sort(t.ascending),this.applyDataLayerZIndexesToDataLayerLayouts()},n.Panel.prototype.getLinkedPanelIds=function(t){t=t||null;var e=[];return["x","y1","y2"].indexOf(t)===-1?e:this.layout.interaction[t+"_linked"]?(this.parent.panel_ids_by_y_index.forEach(function(a){a!==this.id&&this.parent.panels[a].layout.interaction[t+"_linked"]&&e.push(a)}.bind(this)),e):e},n.Panel.prototype.moveUp=function(){return this.parent.panel_ids_by_y_index[this.layout.y_index-1]&&(this.parent.panel_ids_by_y_index[this.layout.y_index]=this.parent.panel_ids_by_y_index[this.layout.y_index-1],this.parent.panel_ids_by_y_index[this.layout.y_index-1]=this.id,this.parent.applyPanelYIndexesToPanelLayouts(),this.parent.positionPanels()),this},n.Panel.prototype.moveDown=function(){return this.parent.panel_ids_by_y_index[this.layout.y_index+1]&&(this.parent.panel_ids_by_y_index[this.layout.y_index]=this.parent.panel_ids_by_y_index[this.layout.y_index+1],this.parent.panel_ids_by_y_index[this.layout.y_index+1]=this.id,this.parent.applyPanelYIndexesToPanelLayouts(),this.parent.positionPanels()),this},n.Panel.prototype.addDataLayer=function(t){if("object"!=typeof t||"string"!=typeof t.id||!t.id.length)throw"Invalid data layer layout passed to LocusZoom.Panel.prototype.addDataLayer()";if("undefined"!=typeof this.data_layers[t.id])throw"Cannot create data_layer with id ["+t.id+"]; data layer with that id already exists in the panel";if("string"!=typeof t.type)throw"Invalid data layer type in layout passed to LocusZoom.Panel.prototype.addDataLayer()";"object"!=typeof t.y_axis||"undefined"!=typeof t.y_axis.axis&&[1,2].indexOf(t.y_axis.axis)!==-1||(t.y_axis.axis=1);var e=n.DataLayers.get(t.type,t,this);if(this.data_layers[e.id]=e,null!==e.layout.z_index&&!isNaN(e.layout.z_index)&&this.data_layer_ids_by_z_index.length>0)e.layout.z_index<0&&(e.layout.z_index=Math.max(this.data_layer_ids_by_z_index.length+e.layout.z_index,0)),this.data_layer_ids_by_z_index.splice(e.layout.z_index,0,e.id),this.data_layer_ids_by_z_index.forEach(function(t,e){this.data_layers[t].layout.z_index=e}.bind(this));else{var a=this.data_layer_ids_by_z_index.push(e.id);this.data_layers[e.id].layout.z_index=a-1}var i=null;return this.layout.data_layers.forEach(function(t,a){t.id===e.id&&(i=a)}),null===i&&(i=this.layout.data_layers.push(this.data_layers[e.id].layout)-1),this.data_layers[e.id].layout_idx=i,this.data_layers[e.id]},n.Panel.prototype.removeDataLayer=function(t){if(!this.data_layers[t])throw"Unable to remove data layer, ID not found: "+t;return this.data_layers[t].destroyAllTooltips(),this.data_layers[t].svg.container&&this.data_layers[t].svg.container.remove(),this.layout.data_layers.splice(this.data_layers[t].layout_idx,1),delete this.state[this.data_layers[t].state_id],delete this.data_layers[t],this.data_layer_ids_by_z_index.splice(this.data_layer_ids_by_z_index.indexOf(t),1),this.applyDataLayerZIndexesToDataLayerLayouts(),this.layout.data_layers.forEach(function(t,e){this.data_layers[t.id].layout_idx=e}.bind(this)),this},n.Panel.prototype.clearSelections=function(){return this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].setAllElementStatus("selected",!1)}.bind(this)),this},n.Panel.prototype.reMap=function(){this.emit("data_requested"),this.data_promises=[],this.curtain.hide();for(var t in this.data_layers)try{this.data_promises.push(this.data_layers[t].reMap())}catch(t){console.warn(t),this.curtain.show(t)}return e.all(this.data_promises).then(function(){this.initialized=!0,this.render(),this.emit("layout_changed"),this.parent.emit("layout_changed"),this.emit("data_rendered")}.bind(this)).catch(function(t){console.warn(t),this.curtain.show(t)}.bind(this))};n.Panel.prototype.generateExtents=function(){["x","y1","y2"].forEach(function(t){this[t+"_extent"]=null}.bind(this));for(var e in this.data_layers){var a=this.data_layers[e];if(a.layout.x_axis&&!a.layout.x_axis.decoupled&&(this.x_extent=t.extent((this.x_extent||[]).concat(a.getAxisExtent("x")))),a.layout.y_axis&&!a.layout.y_axis.decoupled){var i="y"+a.layout.y_axis.axis;this[i+"_extent"]=t.extent((this[i+"_extent"]||[]).concat(a.getAxisExtent("y")))}}return this.layout.axes.x&&"state"===this.layout.axes.x.extent&&(this.x_extent=[this.state.start,this.state.end]),this};n.Panel.prototype.generateTicks=function(t){if(this.layout.axes[t].ticks){var e=this.layout.axes[t],a=e.ticks;if(Array.isArray(a))return a;if("object"==typeof a){var i=this,s={position:a.position},o=this.data_layer_ids_by_z_index.reduce(function(e,a){var n=i.data_layers[a];return e.concat(n.getTicks(t,s))},[]);return o.map(function(t){var e={};return e=n.Layouts.merge(e,a),n.Layouts.merge(e,t)})}}return this[t+"_extent"]?n.prettyTicks(this[t+"_extent"],"both"):[]},n.Panel.prototype.render=function(){this.svg.container.attr("transform","translate("+this.layout.origin.x+","+this.layout.origin.y+")"),this.svg.clipRect.attr("width",this.layout.width).attr("height",this.layout.height),this.inner_border.attr("x",this.layout.margin.left).attr("y",this.layout.margin.top).attr("width",this.layout.width-(this.layout.margin.left+this.layout.margin.right)).attr("height",this.layout.height-(this.layout.margin.top+this.layout.margin.bottom)),this.layout.inner_border&&this.inner_border.style({"stroke-width":1,stroke:this.layout.inner_border}),this.setTitle(),this.generateExtents();var e=function(t,e){var a=Math.pow(-10,e),i=Math.pow(-10,-e),n=Math.pow(10,-e),s=Math.pow(10,e);return t===1/0&&(t=s),t===-(1/0)&&(t=a),0===t&&(t=n),t>0&&(t=Math.max(Math.min(t,s),n)),t<0&&(t=Math.max(Math.min(t,i),a)),t},a={};if(this.x_extent){var i={start:0,end:this.layout.cliparea.width};this.layout.axes.x.range&&(i.start=this.layout.axes.x.range.start||i.start,i.end=this.layout.axes.x.range.end||i.end),a.x=[i.start,i.end],a.x_shifted=[i.start,i.end]}if(this.y1_extent){var n={start:this.layout.cliparea.height,end:0};this.layout.axes.y1.range&&(n.start=this.layout.axes.y1.range.start||n.start,n.end=this.layout.axes.y1.range.end||n.end),a.y1=[n.start,n.end],a.y1_shifted=[n.start,n.end]}if(this.y2_extent){var s={start:this.layout.cliparea.height,end:0};this.layout.axes.y2.range&&(s.start=this.layout.axes.y2.range.start||s.start,s.end=this.layout.axes.y2.range.end||s.end),a.y2=[s.start,s.end],a.y2_shifted=[s.start,s.end]}if(this.parent.interaction.panel_id&&(this.parent.interaction.panel_id===this.id||this.parent.interaction.linked_panel_ids.indexOf(this.id)!==-1)){var o,r=null;if(this.parent.interaction.zooming&&"function"==typeof this.x_scale){var l=Math.abs(this.x_extent[1]-this.x_extent[0]),h=Math.round(this.x_scale.invert(a.x_shifted[1]))-Math.round(this.x_scale.invert(a.x_shifted[0])),d=this.parent.interaction.zooming.scale,u=Math.floor(h*(1/d));d<1&&!isNaN(this.parent.layout.max_region_scale)?d=1/(Math.min(u,this.parent.layout.max_region_scale)/h):d>1&&!isNaN(this.parent.layout.min_region_scale)&&(d=1/(Math.max(u,this.parent.layout.min_region_scale)/h));var p=Math.floor(l*d);o=this.parent.interaction.zooming.center-this.layout.margin.left-this.layout.origin.x;var c=o/this.layout.cliparea.width,y=Math.max(Math.floor(this.x_scale.invert(a.x_shifted[0])-(p-h)*c),1);a.x_shifted=[this.x_scale(y),this.x_scale(y+p)]}else if(this.parent.interaction.dragging)switch(this.parent.interaction.dragging.method){case"background":a.x_shifted[0]=+this.parent.interaction.dragging.dragged_x,a.x_shifted[1]=this.layout.cliparea.width+this.parent.interaction.dragging.dragged_x;break;case"x_tick":t.event&&t.event.shiftKey?(a.x_shifted[0]=+this.parent.interaction.dragging.dragged_x,a.x_shifted[1]=this.layout.cliparea.width+this.parent.interaction.dragging.dragged_x):(o=this.parent.interaction.dragging.start_x-this.layout.margin.left-this.layout.origin.x,r=e(o/(o+this.parent.interaction.dragging.dragged_x),3),a.x_shifted[0]=0,a.x_shifted[1]=Math.max(this.layout.cliparea.width*(1/r),1));break;case"y1_tick":case"y2_tick":var f="y"+this.parent.interaction.dragging.method[1]+"_shifted";t.event&&t.event.shiftKey?(a[f][0]=this.layout.cliparea.height+this.parent.interaction.dragging.dragged_y,a[f][1]=+this.parent.interaction.dragging.dragged_y):(o=this.layout.cliparea.height-(this.parent.interaction.dragging.start_y-this.layout.margin.top-this.layout.origin.y),r=e(o/(o-this.parent.interaction.dragging.dragged_y),3),a[f][0]=this.layout.cliparea.height,a[f][1]=this.layout.cliparea.height-this.layout.cliparea.height*(1/r))}}if(["x","y1","y2"].forEach(function(e){this[e+"_extent"]&&(this[e+"_scale"]=t.scale.linear().domain(this[e+"_extent"]).range(a[e+"_shifted"]),this[e+"_extent"]=[this[e+"_scale"].invert(a[e][0]),this[e+"_scale"].invert(a[e][1])],this[e+"_scale"]=t.scale.linear().domain(this[e+"_extent"]).range(a[e]),this.renderAxis(e))}.bind(this)),this.layout.interaction.scroll_to_zoom){var g=function(){if(!t.event.shiftKey)return void(this.parent.canInteract(this.id)&&this.loader.show("Press [SHIFT] while scrolling to zoom").hide(1e3));if(t.event.preventDefault(),this.parent.canInteract(this.id)){var e=t.mouse(this.svg.container.node()),a=Math.max(-1,Math.min(1,t.event.wheelDelta||-t.event.detail||-t.event.deltaY));0!==a&&(this.parent.interaction={panel_id:this.id,linked_panel_ids:this.getLinkedPanelIds("x"),zooming:{scale:a<1?.9:1.1,center:e[0]}},this.render(),this.parent.interaction.linked_panel_ids.forEach(function(t){this.parent.panels[t].render()}.bind(this)),null!==this.zoom_timeout&&clearTimeout(this.zoom_timeout),this.zoom_timeout=setTimeout(function(){this.parent.interaction={},this.parent.applyState({start:this.x_extent[0],end:this.x_extent[1]})}.bind(this),500))}}.bind(this);this.zoom_listener=t.behavior.zoom(),this.svg.container.call(this.zoom_listener).on("wheel.zoom",g).on("mousewheel.zoom",g).on("DOMMouseScroll.zoom",g)}return this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].draw().render()}.bind(this)),this},n.Panel.prototype.renderAxis=function(e){if(["x","y1","y2"].indexOf(e)===-1)throw"Unable to render axis; invalid axis identifier: "+e;var a=this.layout.axes[e].render&&"function"==typeof this[e+"_scale"]&&!isNaN(this[e+"_scale"](0));if(this[e+"_axis"]&&this.svg.container.select("g.lz-axis.lz-"+e).style("display",a?null:"none"),!a)return this;var i={x:{position:"translate("+this.layout.margin.left+","+(this.layout.height-this.layout.margin.bottom)+")",orientation:"bottom",label_x:this.layout.cliparea.width/2,label_y:this.layout.axes[e].label_offset||0,label_rotate:null},y1:{position:"translate("+this.layout.margin.left+","+this.layout.margin.top+")",orientation:"left",label_x:-1*(this.layout.axes[e].label_offset||0),label_y:this.layout.cliparea.height/2,label_rotate:-90},y2:{position:"translate("+(this.layout.width-this.layout.margin.right)+","+this.layout.margin.top+")",orientation:"right",label_x:this.layout.axes[e].label_offset||0,label_y:this.layout.cliparea.height/2,label_rotate:-90}};this[e+"_ticks"]=this.generateTicks(e);var s=function(t){for(var e=0;e+a[e]&&(n=!0)}),n};try{var i="3.5.6";if("object"!=typeof t)throw"d3 dependency not met. Library missing.";if(!a(i,t.version))throw"d3 dependency not met. Outdated version detected.\nRequired d3 version: "+i+" or higher (found: "+t.version+").";if("function"!=typeof e)throw"Q dependency not met. Library missing.";var n={version:"0.7.2"};n.populate=function(e,a,i){if("undefined"==typeof e)throw"LocusZoom.populate selector not defined";t.select(e).html("");var s;return t.select(e).call(function(){if("undefined"==typeof this.node().id){for(var e=0;!t.select("#lz-"+e).empty();)e++;this.attr("id","#lz-"+e)}if(s=new n.Plot(this.node().id,a,i),s.container=this.node(),"undefined"!=typeof this.node().dataset&&"undefined"!=typeof this.node().dataset.region){var o=n.parsePositionQuery(this.node().dataset.region);Object.keys(o).forEach(function(t){s.state[t]=o[t]})}s.svg=t.select("div#"+s.id).append("svg").attr("version","1.1").attr("xmlns","http://www.w3.org/2000/svg").attr("id",s.id+"_svg").attr("class","lz-locuszoom").style(s.layout.style),s.setDimensions(),s.positionPanels(),s.initialize(),"object"==typeof a&&Object.keys(a).length&&s.refresh()}),s},n.populateAll=function(e,a,i){var s=[];return t.selectAll(e).each(function(t,e){s[e]=n.populate(this,a,i)}),s},n.positionIntToString=function(t,e,a){var i={0:"",3:"K",6:"M",9:"G"};if(a=a||!1,isNaN(e)||null===e){var n=Math.log(t)/Math.LN10;e=Math.min(Math.max(n-n%3,0),9)}var s=e-Math.floor((Math.log(t)/Math.LN10).toFixed(e+3)),o=Math.min(Math.max(e,0),2),r=Math.min(Math.max(s,o),12),l=""+(t/Math.pow(10,e)).toFixed(r);return a&&"undefined"!=typeof i[e]&&(l+=" "+i[e]+"b"),l},n.positionStringToInt=function(t){var e=t.toUpperCase();e=e.replace(/,/g,"");var a=/([KMG])[B]*$/,i=a.exec(e),n=1;return i&&(n="M"===i[1]?1e6:"G"===i[1]?1e9:1e3,e=e.replace(a,"")),e=Number(e)*n},n.parsePositionQuery=function(t){var e=/^(\w+):([\d,.]+[kmgbKMGB]*)([-+])([\d,.]+[kmgbKMGB]*)$/,a=/^(\w+):([\d,.]+[kmgbKMGB]*)$/,i=e.exec(t);if(i){if("+"===i[3]){var s=n.positionStringToInt(i[2]),o=n.positionStringToInt(i[4]);return{chr:i[1],start:s-o,end:s+o}}return{chr:i[1],start:n.positionStringToInt(i[2]),end:n.positionStringToInt(i[4])}}return i=a.exec(t),i?{chr:i[1],position:n.positionStringToInt(i[2])}:null},n.prettyTicks=function(t,e,a){("undefined"==typeof a||isNaN(parseInt(a)))&&(a=5),a=parseInt(a);var i=a/3,n=.75,s=1.5,o=.5+1.5*s,r=Math.abs(t[0]-t[1]),l=r/a;Math.log(r)/Math.LN10<-2&&(l=Math.max(Math.abs(r))*n/i);var h=Math.pow(10,Math.floor(Math.log(l)/Math.LN10)),d=0;h<1&&0!==h&&(d=Math.abs(Math.round(Math.log(h)/Math.LN10)));var u=h;2*h-l0&&(c=parseFloat(c.toFixed(d)));return p.push(c),"undefined"!=typeof e&&["low","high","both","neither"].indexOf(e)!==-1||(e="neither"),"low"!==e&&"both"!==e||p[0]t[1]&&p.pop(),p},n.createCORSPromise=function(t,a,i,n,s){var o=e.defer(),r=new XMLHttpRequest;if("withCredentials"in r?r.open(t,a,!0):"undefined"!=typeof XDomainRequest?(r=new XDomainRequest,r.open(t,a)):r=null,r){if(r.onreadystatechange=function(){4===r.readyState&&(200===r.status||0===r.status?o.resolve(r.response):o.reject("HTTP "+r.status+" for "+a))},s&&setTimeout(o.reject,s),i="undefined"!=typeof i?i:"","undefined"!=typeof n)for(var l in n)r.setRequestHeader(l,n[l]);r.send(i)}return o.promise},n.validateState=function(t,e){t=t||{},e=e||{};var a=!1;if("undefined"!=typeof t.chr&&"undefined"!=typeof t.start&&"undefined"!=typeof t.end){var i,n=null;if(t.start=Math.max(parseInt(t.start),1),t.end=Math.max(parseInt(t.end),1),isNaN(t.start)&&isNaN(t.end))t.start=1,t.end=1,n=.5,i=0;else if(isNaN(t.start)||isNaN(t.end))n=t.start||t.end,i=0,t.start=isNaN(t.start)?t.end:t.start,t.end=isNaN(t.end)?t.start:t.end;else{if(n=Math.round((t.start+t.end)/2),i=t.end-t.start,i<0){var s=t.start;t.end=t.start,t.start=s,i=t.end-t.start}n<0&&(t.start=1,t.end=1,i=0)}a=!0}return!isNaN(e.min_region_scale)&&a&&ie.max_region_scale&&(t.start=Math.max(n-Math.floor(e.max_region_scale/2),1),t.end=t.start+e.max_region_scale),t},n.parseFields=function(t,e){if("object"!=typeof t)throw"LocusZoom.parseFields invalid arguments: data is not an object";if("string"!=typeof e)throw"LocusZoom.parseFields invalid arguments: html is not a string";for(var a=[],i=/\{\{(?:(#if )?([A-Za-z0-9_:|]+)|(\/if))\}\}/;e.length>0;){var s=i.exec(e);s?0!==s.index?(a.push({text:e.slice(0,s.index)}),e=e.slice(s.index)):"#if "===s[1]?(a.push({condition:s[2]}),e=e.slice(s[0].length)):s[2]?(a.push({variable:s[2]}),e=e.slice(s[0].length)):"/if"===s[3]?(a.push({close:"if"}),e=e.slice(s[0].length)):(console.error("Error tokenizing tooltip when remaining template is "+JSON.stringify(e)+" and previous tokens are "+JSON.stringify(a)+" and current regex match is "+JSON.stringify([s[1],s[2],s[3]])),e=e.slice(s[0].length)):(a.push({text:e}),e="")}for(var o=function(){var t=a.shift();if("undefined"!=typeof t.text||t.variable)return t;if(t.condition){for(t.then=[];a.length>0;){if("if"===a[0].close){a.shift();break}t.then.push(o())}return t}return console.error("Error making tooltip AST due to unknown token "+JSON.stringify(t)),{text:""}},r=[];a.length>0;)r.push(o());var l=function(e){return l.cache.hasOwnProperty(e)||(l.cache[e]=new n.Data.Field(e).resolve(t)),l.cache[e]};l.cache={};var h=function(t){if("undefined"!=typeof t.text)return t.text;if(t.variable){try{var e=l(t.variable);if(["string","number","boolean"].indexOf(typeof e)!==-1)return e;if(null===e)return""}catch(e){console.error("Error while processing variable "+JSON.stringify(t.variable))}return"{{"+t.variable+"}}"}if(t.condition){try{var a=l(t.condition);if(a||0===a)return t.then.map(h).join("")}catch(e){console.error("Error while processing condition "+JSON.stringify(t.variable))}return""}console.error("Error rendering tooltip due to unknown AST node "+JSON.stringify(t))};return r.map(h).join("")},n.getToolTipData=function(e){if("object"!=typeof e||"undefined"==typeof e.parentNode)throw"Invalid node object";var a=t.select(e);return a.classed("lz-data_layer-tooltip")&&"undefined"!=typeof a.data()[0]?a.data()[0]:n.getToolTipData(e.parentNode)},n.getToolTipDataLayer=function(t){var e=n.getToolTipData(t);return e.getDataLayer?e.getDataLayer():null},n.getToolTipPanel=function(t){var e=n.getToolTipDataLayer(t);return e?e.parent:null},n.getToolTipPlot=function(t){var e=n.getToolTipPanel(t);return e?e.parent:null},n.generateCurtain=function(){var e={showing:!1,selector:null,content_selector:null,hide_delay:null,show:function(e,a){return this.curtain.showing||(this.curtain.selector=t.select(this.parent_plot.svg.node().parentNode).insert("div").attr("class","lz-curtain").attr("id",this.id+".curtain"),this.curtain.content_selector=this.curtain.selector.append("div").attr("class","lz-curtain-content"),this.curtain.selector.append("div").attr("class","lz-curtain-dismiss").html("Dismiss").on("click",function(){this.curtain.hide()}.bind(this)),this.curtain.showing=!0),this.curtain.update(e,a)}.bind(this),update:function(t,e){if(!this.curtain.showing)return this.curtain;clearTimeout(this.curtain.hide_delay),"object"==typeof e&&this.curtain.selector.style(e);var a=this.getPageOrigin();return this.curtain.selector.style({top:a.y+"px",left:a.x+"px",width:this.layout.width+"px",height:this.layout.height+"px"}),this.curtain.content_selector.style({"max-width":this.layout.width-40+"px","max-height":this.layout.height-40+"px"}),"string"==typeof t&&this.curtain.content_selector.html(t),this.curtain}.bind(this),hide:function(t){return this.curtain.showing?"number"==typeof t?(clearTimeout(this.curtain.hide_delay),this.curtain.hide_delay=setTimeout(this.curtain.hide,t),this.curtain):(this.curtain.selector.remove(),this.curtain.selector=null,this.curtain.content_selector=null,this.curtain.showing=!1,this.curtain):this.curtain}.bind(this)};return e},n.generateLoader=function(){var e={showing:!1,selector:null,content_selector:null,progress_selector:null,cancel_selector:null,show:function(e){return this.loader.showing||(this.loader.selector=t.select(this.parent_plot.svg.node().parentNode).insert("div").attr("class","lz-loader").attr("id",this.id+".loader"),this.loader.content_selector=this.loader.selector.append("div").attr("class","lz-loader-content"),this.loader.progress_selector=this.loader.selector.append("div").attr("class","lz-loader-progress-container").append("div").attr("class","lz-loader-progress"),this.loader.showing=!0,"undefined"==typeof e&&(e="Loading...")),this.loader.update(e)}.bind(this),update:function(t,e){if(!this.loader.showing)return this.loader;clearTimeout(this.loader.hide_delay),"string"==typeof t&&this.loader.content_selector.html(t);var a=6,i=this.getPageOrigin(),n=this.loader.selector.node().getBoundingClientRect();return this.loader.selector.style({top:i.y+this.layout.height-n.height-a+"px",left:i.x+a+"px"}),"number"==typeof e&&this.loader.progress_selector.style({width:Math.min(Math.max(e,1),100)+"%"}),this.loader}.bind(this),animate:function(){return this.loader.progress_selector.classed("lz-loader-progress-animated",!0),this.loader}.bind(this),setPercentCompleted:function(t){return this.loader.progress_selector.classed("lz-loader-progress-animated",!1),this.loader.update(null,t)}.bind(this),hide:function(t){return this.loader.showing?"number"==typeof t?(clearTimeout(this.loader.hide_delay),this.loader.hide_delay=setTimeout(this.loader.hide,t),this.loader):(this.loader.selector.remove(),this.loader.selector=null,this.loader.content_selector=null,this.loader.progress_selector=null,this.loader.cancel_selector=null,this.loader.showing=!1,this.loader):this.loader}.bind(this)};return e},n.subclass=function(t,e,a){if("function"!=typeof t)throw"Parent must be a callable constructor";e=e||{};var i=a||function(){t.apply(this,arguments)};return i.prototype=Object.create(t.prototype),Object.keys(e).forEach(function(t){i.prototype[t]=e[t]}),i.prototype.constructor=i,i},n.Layouts=function(){var t={},e={plot:{},panel:{},data_layer:{},dashboard:{},tooltip:{}};return t.get=function(t,a,i){if("string"!=typeof t||"string"!=typeof a)throw"invalid arguments passed to LocusZoom.Layouts.get, requires string (layout type) and string (layout name)";if(e[t][a]){var s=n.Layouts.merge(i||{},e[t][a]);if(s.unnamespaced)return delete s.unnamespaced,JSON.parse(JSON.stringify(s));var o="";"string"==typeof s.namespace?o=s.namespace:"object"==typeof s.namespace&&Object.keys(s.namespace).length&&(o="undefined"!=typeof s.namespace.default?s.namespace.default:s.namespace[Object.keys(s.namespace)[0]].toString()),o+=o.length?":":"";var r=function(t,e){if(e?"string"==typeof e&&(e={default:e}):e={default:""},"string"==typeof t){for(var a,i,s,l,h=/\{\{namespace(\[[A-Za-z_0-9]+\]|)\}\}/g,d=[];null!==(a=h.exec(t));)i=a[0],s=a[1].length?a[1].replace(/(\[|\])/g,""):null,l=o,null!=e&&"object"==typeof e&&"undefined"!=typeof e[s]&&(l=e[s]+(e[s].length?":":"")),d.push({base:i,namespace:l});for(var u in d)t=t.replace(d[u].base,d[u].namespace)}else if("object"==typeof t&&null!=t){if("undefined"!=typeof t.namespace){var p="string"==typeof t.namespace?{default:t.namespace}:t.namespace;e=n.Layouts.merge(e,p)}var c,y;for(var f in t)"namespace"!==f&&(c=r(t[f],e),y=r(f,e),f!==y&&delete t[f],t[y]=c)}return t};return s=r(s,s.namespace),JSON.parse(JSON.stringify(s))}throw"layout type ["+t+"] name ["+a+"] not found"},t.set=function(t,a,i){if("string"!=typeof t||"string"!=typeof a||"object"!=typeof i)throw"unable to set new layout; bad arguments passed to set()";return e[t]||(e[t]={}),i?e[t][a]=JSON.parse(JSON.stringify(i)):(delete e[t][a],null)},t.add=function(e,a,i){return t.set(e,a,i)},t.list=function(t){if(e[t])return Object.keys(e[t]);var a={};return Object.keys(e).forEach(function(t){a[t]=Object.keys(e[t])}),a},t.merge=function(t,e){if("object"!=typeof t||"object"!=typeof e)throw"LocusZoom.Layouts.merge only accepts two layout objects; "+typeof t+", "+typeof e+" given";for(var a in e)if(e.hasOwnProperty(a)){var i=null===t[a]?"undefined":typeof t[a],s=typeof e[a];if("object"===i&&Array.isArray(t[a])&&(i="array"),"object"===s&&Array.isArray(e[a])&&(s="array"),"function"===i||"function"===s)throw"LocusZoom.Layouts.merge encountered an unsupported property type";"undefined"!==i?"object"!==i||"object"!==s||(t[a]=n.Layouts.merge(t[a],e[a])):t[a]=JSON.parse(JSON.stringify(e[a]))}return t},t}(),n.Layouts.add("tooltip","standard_association",{namespace:{assoc:"assoc"},closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:'{{{{namespace[assoc]}}variant}}
P Value: {{{{namespace[assoc]}}log_pvalue|logtoscinotation}}
Ref. Allele: {{{{namespace[assoc]}}ref_allele}}
Make LD Reference
'});var s=n.Layouts.get("tooltip","standard_association",{unnamespaced:!0});s.html+='Condition on Variant
',n.Layouts.add("tooltip","covariates_model_association",s),n.Layouts.add("tooltip","standard_genes",{closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:'

{{gene_name}}

Gene ID: {{gene_id}}
Transcript ID: {{transcript_id}}
ConstraintExpected variantsObserved variantsConst. Metric
Synonymous{{exp_syn}}{{n_syn}}z = {{syn_z}}
Missense{{exp_mis}}{{n_mis}}z = {{mis_z}}
LoF{{exp_lof}}{{n_lof}}pLI = {{pLI}}
More data on ExAC'}),n.Layouts.add("tooltip","standard_intervals",{namespace:{intervals:"intervals"},closable:!1,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:"{{{{namespace[intervals]}}state_name}}
{{{{namespace[intervals]}}start}}-{{{{namespace[intervals]}}end}}"}),n.Layouts.add("data_layer","significance",{id:"significance",type:"orthogonal_line",orientation:"horizontal",offset:4.522}),n.Layouts.add("data_layer","recomb_rate",{namespace:{recomb:"recomb"},id:"recombrate",type:"line",fields:["{{namespace[recomb]}}position","{{namespace[recomb]}}recomb_rate"],z_index:1,style:{stroke:"#0000FF","stroke-width":"1.5px"},x_axis:{field:"{{namespace[recomb]}}position"},y_axis:{axis:2,field:"{{namespace[recomb]}}recomb_rate",floor:0,ceiling:100}}),n.Layouts.add("data_layer","association_pvalues",{namespace:{assoc:"assoc",ld:"ld"},id:"associationpvalues",type:"scatter",point_shape:{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:"diamond",else:"circle"}},point_size:{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:80,else:40}},color:[{scale_function:"if",field:"{{namespace[ld]}}isrefvar",parameters:{field_value:1,then:"#9632b8"}},{scale_function:"numerical_bin",field:"{{namespace[ld]}}state",parameters:{breaks:[0,.2,.4,.6,.8],values:["#357ebd","#46b8da","#5cb85c","#eea236","#d43f3a"]}},"#B8B8B8"],legend:[{shape:"diamond",color:"#9632b8",size:40,label:"LD Ref Var",class:"lz-data_layer-scatter"},{shape:"circle",color:"#d43f3a",size:40,label:"1.0 > r² ≥ 0.8",class:"lz-data_layer-scatter"},{shape:"circle",color:"#eea236",size:40,label:"0.8 > r² ≥ 0.6",class:"lz-data_layer-scatter"},{shape:"circle",color:"#5cb85c",size:40,label:"0.6 > r² ≥ 0.4",class:"lz-data_layer-scatter"},{shape:"circle",color:"#46b8da",size:40,label:"0.4 > r² ≥ 0.2",class:"lz-data_layer-scatter"},{shape:"circle",color:"#357ebd",size:40,label:"0.2 > r² ≥ 0.0",class:"lz-data_layer-scatter"},{shape:"circle",color:"#B8B8B8",size:40,label:"no r² data",class:"lz-data_layer-scatter"}],fields:["{{namespace[assoc]}}variant","{{namespace[assoc]}}position","{{namespace[assoc]}}log_pvalue","{{namespace[assoc]}}log_pvalue|logtoscinotation","{{namespace[assoc]}}ref_allele","{{namespace[ld]}}state","{{namespace[ld]}}isrefvar"],id_field:"{{namespace[assoc]}}variant",z_index:2,x_axis:{field:"{{namespace[assoc]}}position"},y_axis:{axis:1,field:"{{namespace[assoc]}}log_pvalue",floor:0,upper_buffer:.1,min_extent:[0,10]},behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_association",{unnamespaced:!0})}),n.Layouts.add("data_layer","phewas_pvalues",{namespace:{phewas:"phewas"},id:"phewaspvalues",type:"category_scatter",point_shape:"circle",point_size:70,tooltip_positioning:"vertical",id_field:"{{namespace[phewas]}}id",fields:["{{namespace[phewas]}}id","{{namespace[phewas]}}log_pvalue","{{namespace[phewas]}}trait_group","{{namespace[phewas]}}trait_label"],x_axis:{field:"{{namespace[phewas]}}x",category_field:"{{namespace[phewas]}}trait_group",lower_buffer:.025,upper_buffer:.025},y_axis:{axis:1,field:"{{namespace[phewas]}}log_pvalue",floor:0,upper_buffer:.15},color:{field:"{{namespace[phewas]}}trait_group",scale_function:"categorical_bin",parameters:{categories:[],values:[],null_value:"#B8B8B8"}},fill_opacity:.7,tooltip:{closable:!0,show:{or:["highlighted","selected"]},hide:{and:["unhighlighted","unselected"]},html:["Trait: {{{{namespace[phewas]}}trait_label|htmlescape}}
","Trait Category: {{{{namespace[phewas]}}trait_group|htmlescape}}
","P-value: {{{{namespace[phewas]}}log_pvalue|logtoscinotation|htmlescape}}
"].join("")},behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},label:{text:"{{{{namespace[phewas]}}trait_label}}",spacing:6,lines:{style:{"stroke-width":"2px",stroke:"#333333","stroke-dasharray":"2px 2px"}},filters:[{field:"{{namespace[phewas]}}log_pvalue",operator:">=",value:20}],style:{"font-size":"14px","font-weight":"bold",fill:"#333333"}}}),n.Layouts.add("data_layer","genes",{namespace:{gene:"gene",constraint:"constraint"},id:"genes",type:"genes",fields:["{{namespace[gene]}}gene","{{namespace[constraint]}}constraint"],id_field:"gene_id",behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_genes",{unnamespaced:!0})}),n.Layouts.add("data_layer","genome_legend",{namespace:{genome:"genome"},id:"genome_legend",type:"genome_legend",fields:["{{namespace[genome]}}chr","{{namespace[genome]}}base_pairs"],x_axis:{floor:0,ceiling:2881033286}}),n.Layouts.add("data_layer","intervals",{namespace:{intervals:"intervals"},id:"intervals",type:"intervals",fields:["{{namespace[intervals]}}start","{{namespace[intervals]}}end","{{namespace[intervals]}}state_id","{{namespace[intervals]}}state_name"],id_field:"{{namespace[intervals]}}start",start_field:"{{namespace[intervals]}}start",end_field:"{{namespace[intervals]}}end",track_split_field:"{{namespace[intervals]}}state_id",split_tracks:!0,always_hide_legend:!1,color:{field:"{{namespace[intervals]}}state_id",scale_function:"categorical_bin",parameters:{categories:[1,2,3,4,5,6,7,8,9,10,11,12,13],values:["rgb(212,63,58)","rgb(250,120,105)","rgb(252,168,139)","rgb(240,189,66)","rgb(250,224,105)","rgb(240,238,84)","rgb(244,252,23)","rgb(23,232,252)","rgb(32,191,17)","rgb(23,166,77)","rgb(32,191,17)","rgb(162,133,166)","rgb(212,212,212)"],null_value:"#B8B8B8"}},legend:[{shape:"rect",color:"rgb(212,63,58)",width:9,label:"Active Promoter","{{namespace[intervals]}}state_id":1},{shape:"rect",color:"rgb(250,120,105)",width:9,label:"Weak Promoter","{{namespace[intervals]}}state_id":2},{shape:"rect",color:"rgb(252,168,139)",width:9,label:"Poised Promoter","{{namespace[intervals]}}state_id":3},{shape:"rect",color:"rgb(240,189,66)",width:9,label:"Strong enhancer","{{namespace[intervals]}}state_id":4},{shape:"rect",color:"rgb(250,224,105)",width:9,label:"Strong enhancer","{{namespace[intervals]}}state_id":5},{shape:"rect",color:"rgb(240,238,84)",width:9,label:"Weak enhancer","{{namespace[intervals]}}state_id":6},{shape:"rect",color:"rgb(244,252,23)",width:9,label:"Weak enhancer","{{namespace[intervals]}}state_id":7},{shape:"rect",color:"rgb(23,232,252)",width:9,label:"Insulator","{{namespace[intervals]}}state_id":8},{shape:"rect",color:"rgb(32,191,17)",width:9,label:"Transcriptional transition","{{namespace[intervals]}}state_id":9},{shape:"rect",color:"rgb(23,166,77)",width:9,label:"Transcriptional elongation","{{namespace[intervals]}}state_id":10},{shape:"rect",color:"rgb(136,240,129)",width:9,label:"Weak transcribed","{{namespace[intervals]}}state_id":11},{shape:"rect",color:"rgb(162,133,166)",width:9,label:"Polycomb-repressed","{{namespace[intervals]}}state_id":12},{shape:"rect",color:"rgb(212,212,212)",width:9,label:"Heterochromatin / low signal","{{namespace[intervals]}}state_id":13}],behaviors:{onmouseover:[{action:"set",status:"highlighted"}],onmouseout:[{action:"unset",status:"highlighted"}],onclick:[{action:"toggle",status:"selected",exclusive:!0}],onshiftclick:[{action:"toggle",status:"selected"}]},tooltip:n.Layouts.get("tooltip","standard_intervals",{unnamespaced:!0})}),n.Layouts.add("dashboard","standard_panel",{components:[{type:"remove_panel",position:"right",color:"red",group_position:"end"},{type:"move_panel_up",position:"right",group_position:"middle"},{type:"move_panel_down",position:"right",group_position:"start",style:{"margin-left":"0.75em"}}]}),n.Layouts.add("dashboard","standard_plot",{components:[{type:"title",title:"LocusZoom",subtitle:'v'+n.version+"",position:"left"},{type:"dimensions",position:"right"},{type:"region_scale",position:"right"},{type:"download",position:"right"}]});var o=n.Layouts.get("dashboard","standard_plot");o.components.push({type:"covariates_model",button_html:"Model",button_title:"Show and edit covariates currently in model",position:"left"}),n.Layouts.add("dashboard","covariates_model_plot",o);var r=n.Layouts.get("dashboard","standard_plot");r.components.push({type:"shift_region",step:5e5,button_html:">>",position:"right",group_position:"end"}),r.components.push({type:"shift_region",step:5e4,button_html:">",position:"right",group_position:"middle"}),r.components.push({type:"zoom_region",step:.2,position:"right",group_position:"middle"}),r.components.push({type:"zoom_region",step:-.2,position:"right",group_position:"middle"}),r.components.push({type:"shift_region",step:-5e4,button_html:"<",position:"right",group_position:"middle"}),r.components.push({type:"shift_region",step:-5e5,button_html:"<<",position:"right",group_position:"start"}),n.Layouts.add("dashboard","region_nav_plot",r),n.Layouts.add("panel","association",{id:"association",width:800,height:225,min_width:400,min_height:200,proportional_width:1,margin:{top:35,right:50,bottom:40,left:50},inner_border:"rgb(210, 210, 210)",dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"toggle_legend",position:"right"}),t}(),axes:{x:{label:"Chromosome {{chr}} (Mb)",label_offset:32,tick_format:"region",extent:"state"},y1:{label:"-log10 p-value",label_offset:28},y2:{label:"Recombination Rate (cM/Mb)",label_offset:40}},legend:{orientation:"vertical",origin:{x:55,y:40},hidden:!0},interaction:{drag_background_to_pan:!0,drag_x_ticks_to_scale:!0,drag_y1_ticks_to_scale:!0,drag_y2_ticks_to_scale:!0,scroll_to_zoom:!0,x_linked:!0},data_layers:[n.Layouts.get("data_layer","significance",{unnamespaced:!0}),n.Layouts.get("data_layer","recomb_rate",{unnamespaced:!0}),n.Layouts.get("data_layer","association_pvalues",{unnamespaced:!0})]}),n.Layouts.add("panel","genes",{id:"genes",width:800,height:225,min_width:400,min_height:112.5,proportional_width:1,margin:{top:20,right:50,bottom:20,left:50},axes:{},interaction:{drag_background_to_pan:!0,scroll_to_zoom:!0,x_linked:!0},dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"resize_to_data",position:"right"}),t}(),data_layers:[n.Layouts.get("data_layer","genes",{unnamespaced:!0})]}),n.Layouts.add("panel","phewas",{id:"phewas",width:800,height:300,min_width:800,min_height:300,proportional_width:1,margin:{top:20,right:50,bottom:120,left:50},inner_border:"rgb(210, 210, 210)",axes:{x:{ticks:{style:{"font-weight":"bold","font-size":"11px","text-anchor":"start"},transform:"rotate(50)",position:"left"}},y1:{label:"-log10 p-value",label_offset:28}},data_layers:[n.Layouts.get("data_layer","significance",{unnamespaced:!0}),n.Layouts.get("data_layer","phewas_pvalues",{unnamespaced:!0})]}),n.Layouts.add("panel","genome_legend",{id:"genome_legend",width:800,height:50,origin:{x:0,y:300},min_width:800,min_height:50,proportional_width:1,margin:{top:0,right:50,bottom:35,left:50},axes:{x:{label:"Genomic Position (number denotes chromosome)",label_offset:35,ticks:[{x:124625310,text:"1",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:370850307,text:"2",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:591461209,text:"3",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:786049562,text:"4",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:972084330,text:"5",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1148099493,text:"6",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1313226358,text:"7",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1465977701,text:"8",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1609766427,text:"9",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1748140516,text:"10",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:1883411148,text:"11",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2017840353,text:"12",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2142351240,text:"13",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2253610949,text:"14",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2358551415,text:"15",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2454994487,text:"16",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2540769469,text:"17",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2620405698,text:"18",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2689008813,text:"19",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2750086065,text:"20",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2805663772,text:"21",style:{fill:"rgb(120, 120, 186)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"},{x:2855381003,text:"22",style:{fill:"rgb(0, 0, 66)","text-anchor":"center","font-size":"13px","font-weight":"bold"},transform:"translate(0, 2)"}]}},data_layers:[n.Layouts.get("data_layer","genome_legend",{unnamespaced:!0})]}),n.Layouts.add("panel","intervals",{id:"intervals",width:1e3,height:50,min_width:500,min_height:50,margin:{top:25,right:150,bottom:5,left:50},dashboard:function(){var t=n.Layouts.get("dashboard","standard_panel",{unnamespaced:!0});return t.components.push({type:"toggle_split_tracks",data_layer_id:"intervals",position:"right"}),t}(),axes:{},interaction:{drag_background_to_pan:!0,scroll_to_zoom:!0,x_linked:!0},legend:{hidden:!0,orientation:"horizontal",origin:{x:50,y:0},pad_from_bottom:5},data_layers:[n.Layouts.get("data_layer","intervals",{unnamespaced:!0})]}),n.Layouts.add("plot","standard_association",{state:{},width:800,height:450,responsive_resize:!0,min_region_scale:2e4,max_region_scale:1e6,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","association",{unnamespaced:!0,proportional_height:.5}),n.Layouts.get("panel","genes",{unnamespaced:!0,proportional_height:.5})]}),n.StandardLayout=n.Layouts.get("plot","standard_association"),n.Layouts.add("plot","standard_phewas",{width:800,height:600,min_width:800,min_height:600,responsive_resize:!0,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","phewas",{unnamespaced:!0,proportional_height:.45}),n.Layouts.get("panel","genome_legend",{unnamespaced:!0,proportional_height:.1}),n.Layouts.get("panel","genes",{unnamespaced:!0,proportional_height:.45,margin:{bottom:40},axes:{x:{label:"Chromosome {{chr}} (Mb)",label_offset:32,tick_format:"region",extent:"state"}}})],mouse_guide:!1}),n.Layouts.add("plot","interval_association",{state:{},width:800,height:550,responsive_resize:!0,min_region_scale:2e4,max_region_scale:1e6,dashboard:n.Layouts.get("dashboard","standard_plot",{unnamespaced:!0}),panels:[n.Layouts.get("panel","association",{unnamespaced:!0,width:800,proportional_height:225/570}),n.Layouts.get("panel","intervals",{unnamespaced:!0,proportional_height:120/570}),n.Layouts.get("panel","genes",{unnamespaced:!0,width:800,proportional_height:225/570})]}),n.DataLayer=function(t,e){return this.initialized=!1,this.layout_idx=null,this.id=null,this.parent=e||null,this.svg={},this.parent_plot=null,"undefined"!=typeof e&&e instanceof n.Panel&&(this.parent_plot=e.parent),this.layout=n.Layouts.merge(t||{},n.DataLayer.DefaultLayout),this.layout.id&&(this.id=this.layout.id),this.layout.x_axis!=={}&&"number"!=typeof this.layout.x_axis.axis&&(this.layout.x_axis.axis=1),this.layout.y_axis!=={}&&"number"!=typeof this.layout.y_axis.axis&&(this.layout.y_axis.axis=1),this._base_layout=JSON.parse(JSON.stringify(this.layout)), +this.state={},this.state_id=null,this.setDefaultState(),this.data=[],this.layout.tooltip&&(this.tooltips={}),this.global_statuses={highlighted:!1,selected:!1,faded:!1,hidden:!1},this},n.DataLayer.prototype.addField=function(t,e,a){if(!t||!e)throw"Must specify field name and namespace to use when adding field";var i=e+":"+t;if(a)if(i+="|","string"==typeof a)i+=a;else{if(!Array.isArray(a))throw"Must provide transformations as either a string or array of strings";i+=a.join("|")}var n=this.layout.fields;return n.indexOf(i)===-1&&n.push(i),i},n.DataLayer.prototype.setDefaultState=function(){this.parent&&(this.state=this.parent.state,this.state_id=this.parent.id+"."+this.id,this.state[this.state_id]=this.state[this.state_id]||{},n.DataLayer.Statuses.adjectives.forEach(function(t){this.state[this.state_id][t]=this.state[this.state_id][t]||[]}.bind(this)))},n.DataLayer.DefaultLayout={type:"",fields:[],x_axis:{},y_axis:{}},n.DataLayer.Statuses={verbs:["highlight","select","fade","hide"],adjectives:["highlighted","selected","faded","hidden"],menu_antiverbs:["unhighlight","deselect","unfade","show"]},n.DataLayer.prototype.getBaseId=function(){return this.parent_plot.id+"."+this.parent.id+"."+this.id},n.DataLayer.prototype.getAbsoluteDataHeight=function(){var t=this.svg.group.node().getBoundingClientRect();return t.height},n.DataLayer.prototype.canTransition=function(){return!!this.layout.transition&&!(this.parent_plot.panel_boundaries.dragging||this.parent_plot.interaction.panel_id)},n.DataLayer.prototype.getElementId=function(t){var e="element";if("string"==typeof t)e=t;else if("object"==typeof t){var a=this.layout.id_field||"id";if("undefined"==typeof t[a])throw"Unable to generate element ID";e=t[a].toString().replace(/\W/g,"")}return(this.getBaseId()+"-"+e).replace(/(:|\.|\[|\]|,)/g,"_")},n.DataLayer.prototype.getElementStatusNodeId=function(t){return null},n.DataLayer.prototype.getElementById=function(e){var a=t.select("#"+e.replace(/(:|\.|\[|\]|,)/g,"\\$1"));return!a.empty()&&a.data()&&a.data().length?a.data()[0]:null},n.DataLayer.prototype.applyDataMethods=function(){return this.data.forEach(function(t,e){this.data[e].toHTML=function(){var t=this.layout.id_field||"id",a="";return this.data[e][t]&&(a=this.data[e][t].toString()),a}.bind(this),this.data[e].getDataLayer=function(){return this}.bind(this),this.data[e].deselect=function(){var t=this.getDataLayer();t.unselectElement(this)}}.bind(this)),this.applyCustomDataMethods(),this},n.DataLayer.prototype.applyCustomDataMethods=function(){return this},n.DataLayer.prototype.initialize=function(){return this.svg.container=this.parent.svg.group.append("g").attr("class","lz-data_layer-container").attr("id",this.getBaseId()+".data_layer_container"),this.svg.clipRect=this.svg.container.append("clipPath").attr("id",this.getBaseId()+".clip").append("rect"),this.svg.group=this.svg.container.append("g").attr("id",this.getBaseId()+".data_layer").attr("clip-path","url(#"+this.getBaseId()+".clip)"),this},n.DataLayer.prototype.moveUp=function(){return this.parent.data_layer_ids_by_z_index[this.layout.z_index+1]&&(this.parent.data_layer_ids_by_z_index[this.layout.z_index]=this.parent.data_layer_ids_by_z_index[this.layout.z_index+1],this.parent.data_layer_ids_by_z_index[this.layout.z_index+1]=this.id,this.parent.resortDataLayers()),this},n.DataLayer.prototype.moveDown=function(){return this.parent.data_layer_ids_by_z_index[this.layout.z_index-1]&&(this.parent.data_layer_ids_by_z_index[this.layout.z_index]=this.parent.data_layer_ids_by_z_index[this.layout.z_index-1],this.parent.data_layer_ids_by_z_index[this.layout.z_index-1]=this.id,this.parent.resortDataLayers()),this},n.DataLayer.prototype.resolveScalableParameter=function(t,e){var a=null;if(Array.isArray(t))for(var i=0;null===a&&i":function(t,e){return t>e},">=":function(t,e){return t>=e},"%":function(t,e){return t%e}};return!!Array.isArray(e)&&(2===e.length?t[e[0]]===e[1]:!(3!==e.length||!a[e[1]])&&a[e[1]](t[e[0]],e[2]))},i=[];return this.data.forEach(function(n,s){var o=!0;t.forEach(function(t){a(n,t)||(o=!1)}),o&&i.push("indexes"===e?s:n)}),i},n.DataLayer.prototype.filterIndexes=function(t){return this.filter(t,"indexes")},n.DataLayer.prototype.filterElements=function(t){return this.filter(t,"elements")},n.DataLayer.Statuses.verbs.forEach(function(t,e){var a=n.DataLayer.Statuses.adjectives[e],i="un"+t;n.DataLayer.prototype[t+"Element"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatus(a,t,!0,e),this},n.DataLayer.prototype[i+"Element"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatus(a,t,!1,e),this},n.DataLayer.prototype[t+"ElementsByFilters"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatusByFilters(a,!0,t,e)},n.DataLayer.prototype[i+"ElementsByFilters"]=function(t,e){return e="undefined"!=typeof e&&!!e,this.setElementStatusByFilters(a,!1,t,e)},n.DataLayer.prototype[t+"AllElements"]=function(){return this.setAllElementStatus(a,!0),this},n.DataLayer.prototype[i+"AllElements"]=function(){return this.setAllElementStatus(a,!1),this}}),n.DataLayer.prototype.setElementStatus=function(e,a,i,s){if("undefined"==typeof e||n.DataLayer.Statuses.adjectives.indexOf(e)===-1)throw"Invalid status passed to DataLayer.setElementStatus()";if("undefined"==typeof a)throw"Invalid element passed to DataLayer.setElementStatus()";"undefined"==typeof i&&(i=!0);try{var o=this.getElementId(a)}catch(t){return this}s&&this.setAllElementStatus(e,!i),t.select("#"+o).classed("lz-data_layer-"+this.layout.type+"-"+e,i);var r=this.getElementStatusNodeId(a);null!==r&&t.select("#"+r).classed("lz-data_layer-"+this.layout.type+"-statusnode-"+e,i);var l=this.state[this.state_id][e].indexOf(o);return i&&l===-1&&this.state[this.state_id][e].push(o),i||l===-1||this.state[this.state_id][e].splice(l,1),this.showOrHideTooltip(a),this.parent.emit("layout_changed"),this.parent_plot.emit("layout_changed"),this},n.DataLayer.prototype.setElementStatusByFilters=function(t,e,a,i){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setElementStatusByFilters()";return"undefined"==typeof this.state[this.state_id][t]?this:(e="undefined"==typeof e||!!e,i="undefined"!=typeof i&&!!i,Array.isArray(a)||(a=[]),i&&this.setAllElementStatus(t,!e),this.filterElements(a).forEach(function(a){this.setElementStatus(t,a,e)}.bind(this)),this)},n.DataLayer.prototype.setAllElementStatus=function(t,e){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setAllElementStatus()";if("undefined"==typeof this.state[this.state_id][t])return this;if("undefined"==typeof e&&(e=!0),e)this.data.forEach(function(e){this.setElementStatus(t,e,!0)}.bind(this));else{var a=this.state[this.state_id][t].slice();a.forEach(function(e){var a=this.getElementById(e);"object"==typeof a&&null!==a&&this.setElementStatus(t,a,!1)}.bind(this)),this.state[this.state_id][t]=[]}return this.global_statuses[t]=e,this},n.DataLayer.prototype.applyBehaviors=function(t){"object"==typeof this.layout.behaviors&&Object.keys(this.layout.behaviors).forEach(function(e){var a=/(click|mouseover|mouseout)/.exec(e);a&&t.on(a[0]+"."+e,this.executeBehaviors(e,this.layout.behaviors[e]))}.bind(this))},n.DataLayer.prototype.executeBehaviors=function(e,a){var i={ctrl:e.indexOf("ctrl")!==-1,shift:e.indexOf("shift")!==-1};return function(e){i.ctrl===!!t.event.ctrlKey&&i.shift===!!t.event.shiftKey&&a.forEach(function(t){if("object"==typeof t&&null!==t)switch(t.action){case"set":this.setElementStatus(t.status,e,!0,t.exclusive);break;case"unset":this.setElementStatus(t.status,e,!1,t.exclusive);break;case"toggle":var a=this.state[this.state_id][t.status].indexOf(this.getElementId(e))!==-1,i=t.exclusive&&!a;this.setElementStatus(t.status,e,!a,i);break;case"link":if("string"==typeof t.href){var s=n.parseFields(e,t.href);"string"==typeof t.target?window.open(s,t.target):window.location.href=s}}}.bind(this))}.bind(this)},n.DataLayer.prototype.getPageOrigin=function(){var t=this.parent.getPageOrigin();return{x:t.x+this.parent.layout.margin.left,y:t.y+this.parent.layout.margin.top}},n.DataLayer.prototype.exportData=function(t){var e="json";t=t||e,t="string"==typeof t?t.toLowerCase():e,["json","csv","tsv"].indexOf(t)===-1&&(t=e);var a;switch(t){case"json":try{a=JSON.stringify(this.data)}catch(t){a=null,console.error("Unable to export JSON data from data layer: "+this.getBaseId()+";",t)}break;case"tsv":case"csv":try{var i=JSON.parse(JSON.stringify(this.data));if("object"!=typeof i)a=i.toString();else if(Array.isArray(i)){var n="tsv"===t?"\t":",",s=this.layout.fields.map(function(t){return JSON.stringify(t)}).join(n)+"\n";a=s+i.map(function(t){return this.layout.fields.map(function(e){return"undefined"==typeof t[e]?JSON.stringify(null):"object"==typeof t[e]&&null!==t[e]?Array.isArray(t[e])?'"[Array('+t[e].length+')]"':'"[Object]"':JSON.stringify(t[e])}).join(n)}.bind(this)).join("\n")}else a="Object"}catch(t){a=null,console.error("Unable to export CSV data from data layer: "+this.getBaseId()+";",t)}}return a},n.DataLayer.prototype.draw=function(){return this.svg.container.attr("transform","translate("+this.parent.layout.cliparea.origin.x+","+this.parent.layout.cliparea.origin.y+")"),this.svg.clipRect.attr("width",this.parent.layout.cliparea.width).attr("height",this.parent.layout.cliparea.height),this.positionAllTooltips(),this},n.DataLayer.prototype.reMap=function(){this.destroyAllTooltips();var t=this.parent_plot.lzd.getData(this.state,this.layout.fields);return t.then(function(t){this.data=t.body,this.applyDataMethods(),this.initialized=!0}.bind(this)),t},n.DataLayers=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t]){if("object"!=typeof a)throw"invalid layout argument for data layer ["+t+"]";return new e[t](a,i)}throw"data layer ["+t+"] not found"}return null},t.set=function(t,a){if(a){if("function"!=typeof a)throw"unable to set data layer ["+t+"], argument provided is not a function";e[t]=a,e[t].prototype=new n.DataLayer}else delete e[t]},t.add=function(a,i){if(e[a])throw"data layer already exists with name: "+a;t.set(a,i)},t.extend=function(t,a,i){i=i||{};var s=e[t];if(!s)throw"Attempted to subclass an unknown or unregistered datalayer type";if("object"!=typeof i)throw"Must specify an object of properties and methods";var o=n.subclass(s,i);return e[a]=o,o},t.list=function(){return Object.keys(e)},t}(),n.DataLayers.add("annotation_track",function(t){if(this.DefaultLayout={color:"#000000",filters:[]},t=n.Layouts.merge(t,this.DefaultLayout),!Array.isArray(t.filters))throw"Annotation track must specify array of filters for selecting points to annotate";return n.DataLayer.apply(this,arguments),this.render=function(){var t=this,e=this.filter(this.layout.filters,"elements"),a=this.svg.group.selectAll("rect.lz-data_layer-"+t.layout.type).data(e,function(e){return e[t.layout.id_field]});a.enter().append("rect").attr("class","lz-data_layer-"+this.layout.type).attr("id",function(e){return t.getElementId(e)}),a.attr("x",function(e){return t.parent.x_scale(e[t.layout.x_axis.field])}).attr("width",1).attr("height",t.parent.layout.height).attr("fill",function(e){return t.resolveScalableParameter(t.layout.color,e)}),a.exit().remove(),this.applyBehaviors(a)},this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=7,l=1,h=l/2,d=this.getPageOrigin(),u=o.selector.node().getBoundingClientRect(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=this.parent.x_scale(o.data[this.layout.x_axis.field]),f=p/2,g=Math.max(u.width/2-y,0),_=Math.max(u.width/2+y-c,0);a=d.x+y-u.width/2-_+g,s=u.width/2-r+_-g-h,u.height+l+r>p-f?(e=d.y+f-(u.height+l+r),i="down",n=u.height-l):(e=d.y+f+l+r,i="up",n=0-l-r),o.selector.style("left",a+"px").style("top",e+"px"),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",s+"px").style("top",n+"px")},this}),n.DataLayers.add("forest",function(e){return this.DefaultLayout={point_size:40,point_shape:"square",color:"#888888",fill_opacity:1,y_axis:{axis:2},id_field:"id",confidence_intervals:{start_field:"ci_start",end_field:"ci_end"},show_no_significance_line:!0},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n=this.tooltips[t],s=this.resolveScalableParameter(this.layout.point_size,n.data),o=7,r=1,l=6,h=this.getPageOrigin(),d=this.parent.x_scale(n.data[this.layout.x_axis.field]),u="y"+this.layout.y_axis.axis+"_scale",p=this.parent[u](n.data[this.layout.y_axis.field]),c=n.selector.node().getBoundingClientRect(),y=Math.sqrt(s/Math.PI);d<=this.parent.layout.width/2?(e=h.x+d+y+o+r,a="left",i=-1*(o+r)):(e=h.x+d-c.width-y-o-r,a="right",i=c.width-r);var f,g,_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom);p-c.height/2<=0?(f=h.y+p-1.5*o-l,g=l):p+c.height/2>=_?(f=h.y+p+o+l-c.height,g=c.height-2*o-l):(f=h.y+p-c.height/2,g=c.height/2-o),n.selector.style("left",e+"px").style("top",f+"px"),n.arrow||(n.arrow=n.selector.append("div").style("position","absolute")),n.arrow.attr("class","lz-data_layer-tooltip-arrow_"+a).style("left",i+"px").style("top",g+"px")},this.render=function(){var e="x_scale",a="y"+this.layout.y_axis.axis+"_scale";if(this.layout.confidence_intervals&&this.layout.fields.indexOf(this.layout.confidence_intervals.start_field)!==-1&&this.layout.fields.indexOf(this.layout.confidence_intervals.end_field)!==-1){var i=this.svg.group.selectAll("rect.lz-data_layer-forest.lz-data_layer-forest-ci").data(this.data,function(t){return t[this.layout.id_field]}.bind(this));i.enter().append("rect").attr("class","lz-data_layer-forest lz-data_layer-forest-ci").attr("id",function(t){return this.getElementId(t)+"_ci"}.bind(this)).attr("transform","translate(0,"+(isNaN(this.parent.layout.height)?0:this.parent.layout.height)+")");var n=function(t){var i=this.parent[e](t[this.layout.confidence_intervals.start_field]),n=this.parent[a](t[this.layout.y_axis.field]);return isNaN(i)&&(i=-1e3),isNaN(n)&&(n=-1e3),"translate("+i+","+n+")"}.bind(this),s=function(t){return this.parent[e](t[this.layout.confidence_intervals.end_field])-this.parent[e](t[this.layout.confidence_intervals.start_field])}.bind(this),o=1;this.canTransition()?i.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",n).attr("width",s).attr("height",o):i.attr("transform",n).attr("width",s).attr("height",o),i.exit().remove()}var r=this.svg.group.selectAll("path.lz-data_layer-forest.lz-data_layer-forest-point").data(this.data,function(t){return t[this.layout.id_field]}.bind(this)),l=isNaN(this.parent.layout.height)?0:this.parent.layout.height;r.enter().append("path").attr("class","lz-data_layer-forest lz-data_layer-forest-point").attr("id",function(t){return this.getElementId(t)+"_point"}.bind(this)).attr("transform","translate(0,"+l+")");var h=function(t){var i=this.parent[e](t[this.layout.x_axis.field]),n=this.parent[a](t[this.layout.y_axis.field]);return isNaN(i)&&(i=-1e3),isNaN(n)&&(n=-1e3),"translate("+i+","+n+")"}.bind(this),d=function(t){return this.resolveScalableParameter(this.layout.color,t)}.bind(this),u=function(t){return this.resolveScalableParameter(this.layout.fill_opacity,t)}.bind(this),p=t.svg.symbol().size(function(t){return this.resolveScalableParameter(this.layout.point_size,t)}.bind(this)).type(function(t){return this.resolveScalableParameter(this.layout.point_shape,t)}.bind(this));this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p):r.attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p),r.exit().remove(),r.on("click.event_emitter",function(t){this.parent.emit("element_clicked",t),this.parent_plot.emit("element_clicked",t)}.bind(this)),this.applyBehaviors(r)},this}),n.DataLayers.add("genes",function(e){return this.DefaultLayout={label_font_size:12,label_exon_spacing:4,exon_height:16,bounding_box_padding:6,track_vertical_spacing:10},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.getElementStatusNodeId=function(t){return this.getElementId(t)+"-statusnode"},this.getTrackHeight=function(){return 2*this.layout.bounding_box_padding+this.layout.label_font_size+this.layout.label_exon_spacing+this.layout.exon_height+this.layout.track_vertical_spacing},this.transcript_idx=0,this.tracks=1,this.gene_track_index={1:[]},this.assignTracks=function(){return this.getLabelWidth=function(t,e){try{var a=this.svg.group.append("text").attr("x",0).attr("y",0).attr("class","lz-data_layer-genes lz-label").style("font-size",e).text(t+"→"),i=a.node().getBBox().width;return a.remove(),i}catch(t){return 0}},this.tracks=1,this.gene_track_index={1:[]},this.data.map(function(t,e){if(this.data[e].gene_id&&this.data[e].gene_id.indexOf(".")){var a=this.data[e].gene_id.split(".");this.data[e].gene_id=a[0],this.data[e].gene_version=a[1]}if(this.data[e].transcript_id=this.data[e].transcripts[this.transcript_idx].transcript_id,this.data[e].display_range={start:this.parent.x_scale(Math.max(t.start,this.state.start)),end:this.parent.x_scale(Math.min(t.end,this.state.end))},this.data[e].display_range.label_width=this.getLabelWidth(this.data[e].gene_name,this.layout.label_font_size),this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start,this.data[e].display_range.text_anchor="middle",this.data[e].display_range.widththis.state.end)this.data[e].display_range.start=this.data[e].display_range.end-this.data[e].display_range.label_width-this.layout.label_font_size,this.data[e].display_range.text_anchor="end";else{var i=(this.data[e].display_range.label_width-this.data[e].display_range.width)/2+this.layout.label_font_size;this.data[e].display_range.start-ithis.parent.x_scale(this.state.end)?(this.data[e].display_range.end=this.parent.x_scale(this.state.end),this.data[e].display_range.start=this.data[e].display_range.end-this.data[e].display_range.label_width,this.data[e].display_range.text_anchor="end"):(this.data[e].display_range.start-=i,this.data[e].display_range.end+=i)}this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start}this.data[e].display_range.start-=this.layout.bounding_box_padding,this.data[e].display_range.end+=this.layout.bounding_box_padding,this.data[e].display_range.width+=2*this.layout.bounding_box_padding,this.data[e].display_domain={start:this.parent.x_scale.invert(this.data[e].display_range.start),end:this.parent.x_scale.invert(this.data[e].display_range.end)},this.data[e].display_domain.width=this.data[e].display_domain.end-this.data[e].display_domain.start,this.data[e].track=null;for(var n=1;null===this.data[e].track;){var s=!1;this.gene_track_index[n].map(function(t){if(!s){var e=Math.min(t.display_range.start,this.display_range.start),a=Math.max(t.display_range.end,this.display_range.end);a-ethis.tracks&&(this.tracks=n,this.gene_track_index[n]=[])):(this.data[e].track=n,this.gene_track_index[n].push(this.data[e]))}this.data[e].parent=this,this.data[e].transcripts.map(function(t,a){this.data[e].transcripts[a].parent=this.data[e],this.data[e].transcripts[a].exons.map(function(t,i){this.data[e].transcripts[a].exons[i].parent=this.data[e].transcripts[a]}.bind(this))}.bind(this))}.bind(this)),this},this.render=function(){this.assignTracks();var e,a,i,n,s=this.svg.group.selectAll("g.lz-data_layer-genes").data(this.data,function(t){return t.gene_name});s.enter().append("g").attr("class","lz-data_layer-genes"),s.attr("id",function(t){return this.getElementId(t)}.bind(this)).each(function(s){var o=s.parent,r=t.select(this).selectAll("rect.lz-data_layer-genes.lz-data_layer-genes-statusnode").data([s],function(t){return o.getElementStatusNodeId(t)});r.enter().append("rect").attr("class","lz-data_layer-genes lz-data_layer-genes-statusnode"),r.attr("id",function(t){return o.getElementStatusNodeId(t)}).attr("rx",function(){return o.layout.bounding_box_padding}).attr("ry",function(){return o.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return o.getTrackHeight()-o.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*o.getTrackHeight()},o.canTransition()?r.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):r.attr("width",e).attr("height",a).attr("x",i).attr("y",n),r.exit().remove();var l=t.select(this).selectAll("rect.lz-data_layer-genes.lz-boundary").data([s],function(t){return t.gene_name+"_boundary"});l.enter().append("rect").attr("class","lz-data_layer-genes lz-boundary"),e=function(t){return o.parent.x_scale(t.end)-o.parent.x_scale(t.start)},a=function(){return 1},i=function(t){return o.parent.x_scale(t.start)},n=function(t){return(t.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size+o.layout.label_exon_spacing+Math.max(o.layout.exon_height,3)/2},o.canTransition()?l.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):l.attr("width",e).attr("height",a).attr("x",i).attr("y",n),l.exit().remove();var h=t.select(this).selectAll("text.lz-data_layer-genes.lz-label").data([s],function(t){return t.gene_name+"_label"});h.enter().append("text").attr("class","lz-data_layer-genes lz-label"),h.attr("text-anchor",function(t){return t.display_range.text_anchor}).text(function(t){return"+"===t.strand?t.gene_name+"→":"←"+t.gene_name}).style("font-size",s.parent.layout.label_font_size),i=function(t){return"middle"===t.display_range.text_anchor?t.display_range.start+t.display_range.width/2:"start"===t.display_range.text_anchor?t.display_range.start+o.layout.bounding_box_padding:"end"===t.display_range.text_anchor?t.display_range.end-o.layout.bounding_box_padding:void 0},n=function(t){return(t.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size},o.canTransition()?h.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("x",i).attr("y",n):h.attr("x",i).attr("y",n),h.exit().remove();var d=t.select(this).selectAll("rect.lz-data_layer-genes.lz-exon").data(s.transcripts[s.parent.transcript_idx].exons,function(t){return t.exon_id});d.enter().append("rect").attr("class","lz-data_layer-genes lz-exon"),e=function(t){return o.parent.x_scale(t.end)-o.parent.x_scale(t.start)},a=function(){return o.layout.exon_height},i=function(t){return o.parent.x_scale(t.start)},n=function(){return(s.track-1)*o.getTrackHeight()+o.layout.bounding_box_padding+o.layout.label_font_size+o.layout.label_exon_spacing},o.canTransition()?d.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):d.attr("width",e).attr("height",a).attr("x",i).attr("y",n),d.exit().remove();var u=t.select(this).selectAll("rect.lz-data_layer-genes.lz-clickarea").data([s],function(t){return t.gene_name+"_clickarea"});u.enter().append("rect").attr("class","lz-data_layer-genes lz-clickarea"),u.attr("id",function(t){return o.getElementId(t)+"_clickarea"}).attr("rx",function(){return o.layout.bounding_box_padding}).attr("ry",function(){return o.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return o.getTrackHeight()-o.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*o.getTrackHeight()},o.canTransition()?u.transition().duration(o.layout.transition.duration||0).ease(o.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):u.attr("width",e).attr("height",a).attr("x",i).attr("y",n),u.exit().remove(),u.on("click.event_emitter",function(t){t.parent.parent.emit("element_clicked",t),t.parent.parent_plot.emit("element_clicked",t)}),o.applyBehaviors(u)}),s.exit().remove()},this.positionTooltip=function(e){if("string"!=typeof e)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[e])throw"Unable to position tooltip: id does not point to a valid tooltip";var a,i,n,s=this.tooltips[e],o=7,r=1,l=this.getPageOrigin(),h=s.selector.node().getBoundingClientRect(),d=this.getElementStatusNodeId(s.data),u=t.select("#"+d).node().getBBox(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=(s.data.display_range.start+s.data.display_range.end)/2-this.layout.bounding_box_padding/2,f=Math.max(h.width/2-y,0),g=Math.max(h.width/2+y-c,0),_=l.x+y-h.width/2-g+f,m=h.width/2-o/2+g-f;h.height+r+o>p-(u.y+u.height)?(a=l.y+u.y-(h.height+r+o),i="down",n=h.height-r):(a=l.y+u.y+u.height+r+o,i="up",n=0-r-o),s.selector.style("left",_+"px").style("top",a+"px"),s.arrow||(s.arrow=s.selector.append("div").style("position","absolute")),s.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",m+"px").style("top",n+"px")},this}),n.DataLayers.add("genome_legend",function(t){return this.DefaultLayout={chromosome_fill_colors:{light:"rgb(155, 155, 188)",dark:"rgb(95, 95, 128)"},chromosome_label_colors:{light:"rgb(120, 120, 186)",dark:"rgb(0, 0, 66)"}},t=n.Layouts.merge(t,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.render=function(){var t=0;this.data.forEach(function(e,a){this.data[a].genome_start=t,this.data[a].genome_end=t+e["genome:base_pairs"],t+=e["genome:base_pairs"]}.bind(this));var e=this.svg.group.selectAll("rect.lz-data_layer-genome_legend").data(this.data,function(t){return t["genome:chr"]});e.enter().append("rect").attr("class","lz-data_layer-genome_legend");var a=this,i=this.parent;e.attr("fill",function(t){return t["genome:chr"]%2?a.layout.chromosome_fill_colors.light:a.layout.chromosome_fill_colors.dark}).attr("x",function(t){return i.x_scale(t.genome_start)}).attr("y",0).attr("width",function(t){return i.x_scale(t["genome:base_pairs"])}).attr("height",i.layout.cliparea.height),e.exit().remove(); +var n=/([^:]+):(\d+)(?:_.*)?/.exec(this.state.variant);if(!n)throw"Genome legend cannot understand the specified variant position";var s=n[1],o=n[2];t=+this.data[s-1].genome_start+ +o;var r=this.svg.group.selectAll("rect.lz-data_layer-genome_legend-marker").data([{start:t,end:t+1}]);r.enter().append("rect").attr("class","lz-data_layer-genome_legend-marker"),r.transition().duration(500).style({fill:"rgba(255, 250, 50, 0.8)",stroke:"rgba(255, 250, 50, 0.8)","stroke-width":"3px"}).attr("x",function(t){return i.x_scale(t.start)}).attr("y",0).attr("width",function(t){return i.x_scale(t.end-t.start)}).attr("height",i.layout.cliparea.height),r.exit().remove()},this}),n.DataLayers.add("intervals",function(e){return this.DefaultLayout={start_field:"start",end_field:"end",track_split_field:"state_id",track_split_order:"DESC",track_split_legend_to_y_axis:2,split_tracks:!0,track_height:15,track_vertical_spacing:3,bounding_box_padding:2,always_hide_legend:!1,color:"#B8B8B8",fill_opacity:1},e=n.Layouts.merge(e,this.DefaultLayout),n.DataLayer.apply(this,arguments),this.getElementStatusNodeId=function(t){return this.layout.split_tracks?(this.getBaseId()+"-statusnode-"+t[this.layout.track_split_field]).replace(/[:.[\],]/g,"_"):this.getElementId(t)+"-statusnode"}.bind(this),this.getTrackHeight=function(){return this.layout.track_height+this.layout.track_vertical_spacing+2*this.layout.bounding_box_padding},this.tracks=1,this.previous_tracks=1,this.interval_track_index={1:[]},this.assignTracks=function(){if(this.previous_tracks=this.tracks,this.tracks=0,this.interval_track_index={1:[]},this.track_split_field_index={},this.layout.track_split_field&&this.layout.split_tracks){this.data.map(function(t){this.track_split_field_index[t[this.layout.track_split_field]]=null}.bind(this));var t=Object.keys(this.track_split_field_index);"DESC"===this.layout.track_split_order&&t.reverse(),t.forEach(function(t){this.track_split_field_index[t]=this.tracks+1,this.interval_track_index[this.tracks+1]=[],this.tracks++}.bind(this))}return this.data.map(function(t,e){if(this.data[e].parent=this,this.data[e].display_range={start:this.parent.x_scale(Math.max(t[this.layout.start_field],this.state.start)),end:this.parent.x_scale(Math.min(t[this.layout.end_field],this.state.end))},this.data[e].display_range.width=this.data[e].display_range.end-this.data[e].display_range.start,this.data[e].display_domain={start:this.parent.x_scale.invert(this.data[e].display_range.start),end:this.parent.x_scale.invert(this.data[e].display_range.end)},this.data[e].display_domain.width=this.data[e].display_domain.end-this.data[e].display_domain.start,this.layout.track_split_field&&this.layout.split_tracks){var a=this.data[e][this.layout.track_split_field];this.data[e].track=this.track_split_field_index[a],this.interval_track_index[this.data[e].track].push(e)}else{this.tracks=1,this.data[e].track=null;for(var i=1;null===this.data[e].track;){var n=!1;this.interval_track_index[i].map(function(t){if(!n){var e=Math.min(t.display_range.start,this.display_range.start),a=Math.max(t.display_range.end,this.display_range.end);a-ethis.tracks&&(this.tracks=i,this.interval_track_index[i]=[])):(this.data[e].track=i,this.interval_track_index[i].push(this.data[e]))}}}.bind(this)),this},this.render=function(){this.assignTracks(),this.svg.group.selectAll(".lz-data_layer-intervals-statusnode.lz-data_layer-intervals-shared").remove(),Object.keys(this.track_split_field_index).forEach(function(t){var e={};e[this.layout.track_split_field]=t;var a={display:this.layout.split_tracks?null:"none"};this.svg.group.insert("rect",":first-child").attr("id",this.getElementStatusNodeId(e)).attr("class","lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-shared").attr("rx",this.layout.bounding_box_padding).attr("ry",this.layout.bounding_box_padding).attr("width",this.parent.layout.cliparea.width).attr("height",this.getTrackHeight()-this.layout.track_vertical_spacing).attr("x",0).attr("y",(this.track_split_field_index[t]-1)*this.getTrackHeight()).style(a)}.bind(this));var e,a,i,n,s,o,r=this.svg.group.selectAll("g.lz-data_layer-intervals").data(this.data,function(t){return t[this.layout.id_field]}.bind(this));return r.enter().append("g").attr("class","lz-data_layer-intervals"),r.attr("id",function(t){return this.getElementId(t)}.bind(this)).each(function(r){var l=r.parent,h={display:l.layout.split_tracks?"none":null},d=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-statusnode-discrete").data([r],function(t){return l.getElementId(t)+"-statusnode"});d.enter().insert("rect",":first-child").attr("class","lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-statusnode-discrete"),d.attr("id",function(t){return l.getElementId(t)+"-statusnode"}).attr("rx",function(){return l.layout.bounding_box_padding}).attr("ry",function(){return l.layout.bounding_box_padding}).style(h),e=function(t){return t.display_range.width+2*l.layout.bounding_box_padding},a=function(){return l.getTrackHeight()-l.layout.track_vertical_spacing},i=function(t){return t.display_range.start-l.layout.bounding_box_padding},n=function(t){return(t.track-1)*l.getTrackHeight()},l.canTransition()?d.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):d.attr("width",e).attr("height",a).attr("x",i).attr("y",n),d.exit().remove();var u=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-interval_rect").data([r],function(t){return t[l.layout.id_field]+"_interval_rect"});u.enter().append("rect").attr("class","lz-data_layer-intervals lz-interval_rect"),a=l.layout.track_height,e=function(t){return t.display_range.width},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*l.getTrackHeight()+l.layout.bounding_box_padding},s=function(t){return l.resolveScalableParameter(l.layout.color,t)},o=function(t){return l.resolveScalableParameter(l.layout.fill_opacity,t)},l.canTransition()?u.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n).attr("fill",s).attr("fill-opacity",o):u.attr("width",e).attr("height",a).attr("x",i).attr("y",n).attr("fill",s).attr("fill-opacity",o),u.exit().remove();var p=t.select(this).selectAll("rect.lz-data_layer-intervals.lz-clickarea").data([r],function(t){return t.interval_name+"_clickarea"});p.enter().append("rect").attr("class","lz-data_layer-intervals lz-clickarea"),p.attr("id",function(t){return l.getElementId(t)+"_clickarea"}).attr("rx",function(){return l.layout.bounding_box_padding}).attr("ry",function(){return l.layout.bounding_box_padding}),e=function(t){return t.display_range.width},a=function(){return l.getTrackHeight()-l.layout.track_vertical_spacing},i=function(t){return t.display_range.start},n=function(t){return(t.track-1)*l.getTrackHeight()},l.canTransition()?p.transition().duration(l.layout.transition.duration||0).ease(l.layout.transition.ease||"cubic-in-out").attr("width",e).attr("height",a).attr("x",i).attr("y",n):p.attr("width",e).attr("height",a).attr("x",i).attr("y",n),p.exit().remove(),p.on("click",function(t){t.parent.parent.emit("element_clicked",t),t.parent.parent_plot.emit("element_clicked",t)}.bind(this)),l.applyBehaviors(p)}),r.exit().remove(),this.previous_tracks!==this.tracks&&this.updateSplitTrackAxis(),this},this.positionTooltip=function(e){if("string"!=typeof e)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[e])throw"Unable to position tooltip: id does not point to a valid tooltip";var a,i,n,s=this.tooltips[e],o=7,r=1,l=this.getPageOrigin(),h=s.selector.node().getBoundingClientRect(),d=t.select("#"+this.getElementStatusNodeId(s.data)).node().getBBox(),u=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),p=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),c=(s.data.display_range.start+s.data.display_range.end)/2-this.layout.bounding_box_padding/2,y=Math.max(h.width/2-c,0),f=Math.max(h.width/2+c-p,0),g=l.x+c-h.width/2-f+y,_=h.width/2-o/2+f-y;h.height+r+o>u-(d.y+d.height)?(a=l.y+d.y-(h.height+r+o),i="down",n=h.height-r):(a=l.y+d.y+d.height+r+o,i="up",n=0-r-o),s.selector.style("left",g+"px").style("top",a+"px"),s.arrow||(s.arrow=s.selector.append("div").style("position","absolute")),s.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",_+"px").style("top",n+"px")},this.updateSplitTrackAxis=function(){var t=!!this.layout.track_split_legend_to_y_axis&&"y"+this.layout.track_split_legend_to_y_axis;if(this.layout.split_tracks){var e=+this.tracks||0,a=+this.layout.track_height||0,i=2*(+this.layout.bounding_box_padding||0)+(+this.layout.track_vertical_spacing||0),n=e*a+(e-1)*i;this.parent.scaleHeightToData(n),t&&this.parent.legend&&(this.parent.legend.hide(),this.parent.layout.axes[t]={render:!0,ticks:[],range:{start:n-this.layout.track_height/2,end:this.layout.track_height/2}},this.layout.legend.forEach(function(a){var i=a[this.layout.track_split_field],n=this.track_split_field_index[i];n&&("DESC"===this.layout.track_split_order&&(n=Math.abs(n-e-1)),this.parent.layout.axes[t].ticks.push({y:n,text:a.label}))}.bind(this)),this.layout.y_axis={axis:this.layout.track_split_legend_to_y_axis,floor:1,ceiling:e},this.parent.render()),this.parent_plot.positionPanels()}else t&&this.parent.legend&&(this.layout.always_hide_legend||this.parent.legend.show(),this.parent.layout.axes[t]={render:!1},this.parent.render());return this},this.toggleSplitTracks=function(){return this.layout.split_tracks=!this.layout.split_tracks,this.parent.legend&&!this.layout.always_hide_legend&&(this.parent.layout.margin.bottom=5+(this.layout.split_tracks?0:this.parent.legend.layout.height+5)),this.render(),this.updateSplitTrackAxis(),this},this}),n.DataLayers.add("line",function(e){return this.DefaultLayout={style:{fill:"none","stroke-width":"2px"},interpolate:"linear",x_axis:{field:"x"},y_axis:{field:"y",axis:1},hitarea_width:5},e=n.Layouts.merge(e,this.DefaultLayout),this.mouse_event=null,this.line=null,this.tooltip_timeout=null,n.DataLayer.apply(this,arguments),this.getMouseDisplayAndData=function(){var e={display:{x:t.mouse(this.mouse_event)[0],y:null},data:{},slope:null},a=this.layout.x_axis.field,i=this.layout.y_axis.field,n="x_scale",s="y"+this.layout.y_axis.axis+"_scale";e.data[a]=this.parent[n].invert(e.display.x);var o=t.bisector(function(t){return+t[a]}).left,r=o(this.data,e.data[a])-1,l=this.data[r],h=this.data[r+1],d=t.interpolateNumber(+l[i],+h[i]),u=+h[a]-+l[a];return e.data[i]=d(e.data[a]%u/u),e.display.y=this.parent[s](e.data[i]),this.layout.tooltip.x_precision&&(e.data[a]=e.data[a].toPrecision(this.layout.tooltip.x_precision)),this.layout.tooltip.y_precision&&(e.data[i]=e.data[i].toPrecision(this.layout.tooltip.y_precision)),e.slope=(this.parent[s](h[i])-this.parent[s](l[i]))/(this.parent[n](h[a])-this.parent[n](l[a])),e},this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=o.selector.node().getBoundingClientRect(),l=7,h=6,d=parseFloat(this.layout.style["stroke-width"])||1,u=this.getPageOrigin(),p=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),c=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right),y=this.getMouseDisplayAndData();if(Math.abs(y.slope)>1)y.display.x<=this.parent.layout.width/2?(a=u.x+y.display.x+d+l+d,s="left",n=-1*(l+d)):(a=u.x+y.display.x-r.width-d-l-d,s="right",n=r.width-d),y.display.y-r.height/2<=0?(e=u.y+y.display.y-1.5*l-h,i=h):y.display.y+r.height/2>=p?(e=u.y+y.display.y+l+h-r.height,i=r.height-2*l-h):(e=u.y+y.display.y-r.height/2,i=r.height/2-l);else{var f=Math.max(r.width/2-y.display.x,0),g=Math.max(r.width/2+y.display.x-c,0);a=u.x+y.display.x-r.width/2-g+f;var _=l/2,m=r.width-2.5*l;n=r.width/2-l+g-f,n=Math.min(Math.max(n,_),m),r.height+d+l>y.display.y?(e=u.y+y.display.y+d+l,s="up",i=0-d-l):(e=u.y+y.display.y-(r.height+d+l),s="down",i=r.height-d)}o.selector.style({left:a+"px",top:e+"px"}),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+s).style({left:n+"px",top:i+"px"})},this.render=function(){var e=this,a=this.parent,i=this.layout.x_axis.field,n=this.layout.y_axis.field,s="x_scale",o="y"+this.layout.y_axis.axis+"_scale",r=this.svg.group.selectAll("path.lz-data_layer-line").data([this.data]);if(this.path=r.enter().append("path").attr("class","lz-data_layer-line"),this.line=t.svg.line().x(function(t){return parseFloat(a[s](t[i]))}).y(function(t){return parseFloat(a[o](t[n]))}).interpolate(this.layout.interpolate),this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("d",this.line).style(this.layout.style):r.attr("d",this.line).style(this.layout.style),this.layout.tooltip){var l=parseFloat(this.layout.hitarea_width).toString()+"px",h=this.svg.group.selectAll("path.lz-data_layer-line-hitarea").data([this.data]);h.enter().append("path").attr("class","lz-data_layer-line-hitarea").style("stroke-width",l);var d=t.svg.line().x(function(t){return parseFloat(a[s](t[i]))}).y(function(t){return parseFloat(a[o](t[n]))}).interpolate(this.layout.interpolate);h.attr("d",d).on("mouseover",function(){clearTimeout(e.tooltip_timeout),e.mouse_event=this;var t=e.getMouseDisplayAndData();e.createTooltip(t.data)}).on("mousemove",function(){clearTimeout(e.tooltip_timeout),e.mouse_event=this;var t=e.getMouseDisplayAndData();e.updateTooltip(t.data),e.positionTooltip(e.getElementId())}).on("mouseout",function(){e.tooltip_timeout=setTimeout(function(){e.mouse_event=null,e.destroyTooltip(e.getElementId())},300)}),h.exit().remove()}r.exit().remove()},this.setElementStatus=function(t,e,a){return this.setAllElementStatus(t,a)},this.setElementStatusByFilters=function(t,e){return this.setAllElementStatus(t,e)},this.setAllElementStatus=function(t,e){if("undefined"==typeof t||n.DataLayer.Statuses.adjectives.indexOf(t)===-1)throw"Invalid status passed to DataLayer.setAllElementStatus()";if("undefined"==typeof this.state[this.state_id][t])return this;"undefined"==typeof e&&(e=!0),this.global_statuses[t]=e;var a="lz-data_layer-line";return Object.keys(this.global_statuses).forEach(function(t){this.global_statuses[t]&&(a+=" lz-data_layer-line-"+t)}.bind(this)),this.path.attr("class",a),this.parent.emit("layout_changed"),this.parent_plot.emit("layout_changed"),this},this}),n.DataLayers.add("orthogonal_line",function(e){return this.DefaultLayout={style:{stroke:"#D3D3D3","stroke-width":"3px","stroke-dasharray":"10px 10px"},orientation:"horizontal",x_axis:{axis:1,decoupled:!0},y_axis:{axis:1,decoupled:!0},offset:0},e=n.Layouts.merge(e,this.DefaultLayout),["horizontal","vertical"].indexOf(e.orientation)===-1&&(e.orientation="horizontal"),this.data=[],this.line=null,n.DataLayer.apply(this,arguments),this.render=function(){var e=this.parent,a="x_scale",i="y"+this.layout.y_axis.axis+"_scale",n="x_extent",s="y"+this.layout.y_axis.axis+"_extent",o="x_range",r="y"+this.layout.y_axis.axis+"_range";"horizontal"===this.layout.orientation?this.data=[{x:e[n][0],y:this.layout.offset},{x:e[n][1],y:this.layout.offset}]:this.data=[{x:this.layout.offset,y:e[s][0]},{x:this.layout.offset,y:e[s][1]}];var l=this.svg.group.selectAll("path.lz-data_layer-line").data([this.data]);this.path=l.enter().append("path").attr("class","lz-data_layer-line"),this.line=t.svg.line().x(function(t,i){var n=parseFloat(e[a](t.x));return isNaN(n)?e[o][i]:n}).y(function(t,a){var n=parseFloat(e[i](t.y));return isNaN(n)?e[r][a]:n}).interpolate("linear"),this.canTransition()?l.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("d",this.line).style(this.layout.style):l.attr("d",this.line).style(this.layout.style),l.exit().remove()},this}),n.DataLayers.add("scatter",function(e){return this.DefaultLayout={point_size:40,point_shape:"circle",tooltip_positioning:"horizontal",color:"#888888",fill_opacity:1,y_axis:{axis:1},id_field:"id"},e=n.Layouts.merge(e,this.DefaultLayout),e.label&&isNaN(e.label.spacing)&&(e.label.spacing=4),n.DataLayer.apply(this,arguments),this.positionTooltip=function(t){if("string"!=typeof t)throw"Unable to position tooltip: id is not a string";if(!this.tooltips[t])throw"Unable to position tooltip: id does not point to a valid tooltip";var e,a,i,n,s,o=this.tooltips[t],r=this.resolveScalableParameter(this.layout.point_size,o.data),l=Math.sqrt(r/Math.PI),h=7,d=1,u=6,p=this.getPageOrigin(),c=this.parent.x_scale(o.data[this.layout.x_axis.field]),y="y"+this.layout.y_axis.axis+"_scale",f=this.parent[y](o.data[this.layout.y_axis.field]),g=o.selector.node().getBoundingClientRect(),_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),m=this.parent.layout.width-(this.parent.layout.margin.left+this.parent.layout.margin.right);if("vertical"===this.layout.tooltip_positioning){var b=Math.max(g.width/2-c,0),x=Math.max(g.width/2+c-m,0);a=p.x+c-g.width/2-x+b,s=g.width/2-h/2+x-b-l,g.height+d+h>_-(f+l)?(e=p.y+f-(l+g.height+d+h),i="down",n=g.height-d):(e=p.y+f+l+d+h,i="up",n=0-d-h)}else c<=this.parent.layout.width/2?(a=p.x+c+l+h+d,i="left",s=-1*(h+d)):(a=p.x+c-g.width-l-h-d,i="right",s=g.width-d),_=this.parent.layout.height-(this.parent.layout.margin.top+this.parent.layout.margin.bottom),f-g.height/2<=0?(e=p.y+f-1.5*h-u,n=u):f+g.height/2>=_?(e=p.y+f+h+u-g.height,n=g.height-2*h-u):(e=p.y+f-g.height/2,n=g.height/2-h);o.selector.style("left",a+"px").style("top",e+"px"),o.arrow||(o.arrow=o.selector.append("div").style("position","absolute")),o.arrow.attr("class","lz-data_layer-tooltip-arrow_"+i).style("left",s+"px").style("top",n+"px")},this.flip_labels=function(){var e=this,a=e.resolveScalableParameter(e.layout.point_size,{}),i=e.layout.label.spacing,n=Boolean(e.layout.label.lines),s=2*i,o=e.parent.layout.width-e.parent.layout.margin.left-e.parent.layout.margin.right-2*i,r=function(t,e){var s=+t.attr("x"),o=2*i+2*Math.sqrt(a);if(n)var r=+e.attr("x2"),l=i+2*Math.sqrt(a);"start"===t.style("text-anchor")?(t.style("text-anchor","end"),t.attr("x",s-o),n&&e.attr("x2",r-l)):(t.style("text-anchor","start"),t.attr("x",s+o),n&&e.attr("x2",r+l))};e.label_texts.each(function(a,s){var l=this,h=t.select(l),d=+h.attr("x"),u=h.node().getBoundingClientRect();if(d+u.width+i>o){var p=n?t.select(e.label_lines[0][s]):null;r(h,p)}}),e.label_texts.each(function(a,o){var l=this,h=t.select(l);if("end"!==h.style("text-anchor")){var d=+h.attr("x"),u=h.node().getBoundingClientRect(),p=n?t.select(e.label_lines[0][o]):null;e.label_texts.each(function(){var e=this,a=t.select(e),n=a.node().getBoundingClientRect(),o=u.leftn.left&&u.topn.top;o&&(r(h,p),d=+h.attr("x"),d-u.width-iu.left&&d.topu.top;if(p){n=!0;var c,y=h.attr("y"),f=d.topx?(c=_-+r,_=+r,m-=c):m+u.height/2>x&&(c=m-+y,m=+y,_-=c),o.attr("y",_),h.attr("y",m)}}}})}),n){if(e.layout.label.lines){var s=e.label_texts[0];e.label_lines.attr("y2",function(e,a){var i=t.select(s[a]);return i.attr("y")})}this.seperate_iterations<150&&setTimeout(function(){this.separate_labels()}.bind(this),1)}},this.render=function(){var e=this,a="x_scale",i="y"+this.layout.y_axis.axis+"_scale";if(this.layout.label){var s=this.data.filter(function(t){if(e.layout.label.filters){var a=!0;return e.layout.label.filters.forEach(function(e){var i=new n.Data.Field(e.field).resolve(t);if(isNaN(i))a=!1;else switch(e.operator){case"<":i":i>e.value||(a=!1);break;case">=":i>=e.value||(a=!1);break;case"=":i!==e.value&&(a=!1);break;default:a=!1}}),a}return!0}),o=this;this.label_groups=this.svg.group.selectAll("g.lz-data_layer-"+this.layout.type+"-label").data(s,function(t){return t[o.layout.id_field]+"_label"}),this.label_groups.enter().append("g").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_texts&&this.label_texts.remove(),this.label_texts=this.label_groups.append("text").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_texts.text(function(t){return n.parseFields(t,e.layout.label.text||"")}).style(e.layout.label.style||{}).attr({x:function(t){var i=e.parent[a](t[e.layout.x_axis.field])+Math.sqrt(e.resolveScalableParameter(e.layout.point_size,t))+e.layout.label.spacing;return isNaN(i)&&(i=-1e3),i},y:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a},"text-anchor":function(){return"start"}}),e.layout.label.lines&&(this.label_lines&&this.label_lines.remove(),this.label_lines=this.label_groups.append("line").attr("class","lz-data_layer-"+this.layout.type+"-label"),this.label_lines.style(e.layout.label.lines.style||{}).attr({x1:function(t){var i=e.parent[a](t[e.layout.x_axis.field]);return isNaN(i)&&(i=-1e3),i},y1:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a},x2:function(t){var i=e.parent[a](t[e.layout.x_axis.field])+Math.sqrt(e.resolveScalableParameter(e.layout.point_size,t))+e.layout.label.spacing/2;return isNaN(i)&&(i=-1e3),i},y2:function(t){var a=e.parent[i](t[e.layout.y_axis.field]);return isNaN(a)&&(a=-1e3),a}})),this.label_groups.exit().remove()}var r=this.svg.group.selectAll("path.lz-data_layer-"+this.layout.type).data(this.data,function(t){return t[this.layout.id_field]}.bind(this)),l=isNaN(this.parent.layout.height)?0:this.parent.layout.height;r.enter().append("path").attr("class","lz-data_layer-"+this.layout.type).attr("id",function(t){return this.getElementId(t)}.bind(this)).attr("transform","translate(0,"+l+")");var h=function(t){var e=this.parent[a](t[this.layout.x_axis.field]),n=this.parent[i](t[this.layout.y_axis.field]);return isNaN(e)&&(e=-1e3),isNaN(n)&&(n=-1e3),"translate("+e+","+n+")"}.bind(this),d=function(t){return this.resolveScalableParameter(this.layout.color,t)}.bind(this),u=function(t){return this.resolveScalableParameter(this.layout.fill_opacity,t)}.bind(this),p=t.svg.symbol().size(function(t){return this.resolveScalableParameter(this.layout.point_size,t)}.bind(this)).type(function(t){return this.resolveScalableParameter(this.layout.point_shape,t)}.bind(this));this.canTransition()?r.transition().duration(this.layout.transition.duration||0).ease(this.layout.transition.ease||"cubic-in-out").attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p):r.attr("transform",h).attr("fill",d).attr("fill-opacity",u).attr("d",p),r.exit().remove(),r.on("click.event_emitter",function(t){this.parent.emit("element_clicked",t),this.parent_plot.emit("element_clicked",t)}.bind(this)),this.applyBehaviors(r),this.layout.label&&(this.flip_labels(),this.seperate_iterations=0,this.separate_labels(),this.label_texts.on("click.event_emitter",function(t){this.parent.emit("element_clicked",t),this.parent_plot.emit("element_clicked",t)}.bind(this)),this.applyBehaviors(this.label_texts))},this.makeLDReference=function(t){var e=null;if("undefined"==typeof t)throw"makeLDReference requires one argument of any type";e="object"==typeof t?this.layout.id_field&&"undefined"!=typeof t[this.layout.id_field]?t[this.layout.id_field].toString():"undefined"!=typeof t.id?t.id.toString():t.toString():t.toString(),this.parent_plot.applyState({ldrefvar:e})},this}),n.DataLayers.extend("scatter","category_scatter",{_prepareData:function(){var t=this.layout.x_axis.field||"x",e=this.layout.x_axis.category_field;if(!e)throw"Layout for "+this.layout.id+" must specify category_field";var a=this.data.sort(function(t,a){var i=t[e],n=a[e],s=i.toString?i.toString().toLowerCase():i,o=n.toString?n.toString().toLowerCase():n;return s===o?0:s1?function(t){for(var e=t,n=0;n1?Math.ceil(Math.log(t)/Math.LN10):Math.floor(Math.log(t)/Math.LN10),Math.abs(e)<=3?t.toFixed(3):t.toExponential(2).replace("+","").replace("e"," × 10^")}),n.TransformationFunctions.add("urlencode",function(t){return encodeURIComponent(t)}),n.TransformationFunctions.add("htmlescape",function(t){return t?(t+="",t.replace(/['"<>&`]/g,function(t){switch(t){case"'":return"'";case'"':return""";case"<":return"<";case">":return">";case"&":return"&";case"`":return"`"}})):""}),n.ScaleFunctions=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t])return"undefined"==typeof a&&"undefined"==typeof i?e[t]:e[t](a,i);throw"scale function ["+t+"] not found"}return null},t.set=function(t,a){a?e[t]=a:delete e[t]},t.add=function(a,i){if(e[a])throw"scale function already exists with name: "+a;t.set(a,i)},t.list=function(){return Object.keys(e)},t}(),n.ScaleFunctions.add("if",function(t,e){return"undefined"==typeof e||t.field_value!==e?"undefined"!=typeof t.else?t.else:null:t.then}),n.ScaleFunctions.add("numerical_bin",function(t,e){var a=t.breaks||[],i=t.values||[];if("undefined"==typeof e||null===e||isNaN(+e))return t.null_value?t.null_value:null;var n=a.reduce(function(t,a){return+e=t&&+e=e.breaks[e.breaks.length-1])return n[i.length-1];var o=null;if(i.forEach(function(t,e){e&&i[e-1]<=+a&&i[e]>=+a&&(o=e)}),null===o)return s;var r=(+a-i[o-1])/(i[o]-i[o-1]);return isFinite(r)?t.interpolate(n[o-1],n[o])(r):s}),n.Dashboard=function(t){if(!(t instanceof n.Plot||t instanceof n.Panel))throw"Unable to create dashboard, parent must be a locuszoom plot or panel";return this.parent=t,this.id=this.parent.getBaseId()+".dashboard",this.type=this.parent instanceof n.Plot?"plot":"panel",this.parent_plot="plot"===this.type?this.parent:this.parent.parent,this.selector=null,this.components=[],this.hide_timeout=null,this.persist=!1,this.initialize()},n.Dashboard.prototype.initialize=function(){return Array.isArray(this.parent.layout.dashboard.components)&&this.parent.layout.dashboard.components.forEach(function(t){try{var e=n.Dashboard.Components.get(t.type,t,this);this.components.push(e)}catch(t){console.warn(t)}}.bind(this)),"panel"===this.type&&(t.select(this.parent.parent.svg.node().parentNode).on("mouseover."+this.id,function(){clearTimeout(this.hide_timeout),this.selector&&"hidden"!==this.selector.style("visibility")||this.show()}.bind(this)),t.select(this.parent.parent.svg.node().parentNode).on("mouseout."+this.id,function(){clearTimeout(this.hide_timeout),this.hide_timeout=setTimeout(function(){this.hide()}.bind(this),300)}.bind(this))),this},n.Dashboard.prototype.shouldPersist=function(){if(this.persist)return!0;var t=!1;return this.components.forEach(function(e){t=t||e.shouldPersist()}),t=t||this.parent_plot.panel_boundaries.dragging||this.parent_plot.interaction.dragging, +!!t},n.Dashboard.prototype.show=function(){if(!this.selector){switch(this.type){case"plot":this.selector=t.select(this.parent.svg.node().parentNode).insert("div",":first-child");break;case"panel":this.selector=t.select(this.parent.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip, .lz-dashboard-menu, .lz-curtain").classed("lz-panel-dashboard",!0)}this.selector.classed("lz-dashboard",!0).classed("lz-"+this.type+"-dashboard",!0).attr("id",this.id)}return this.components.forEach(function(t){t.show()}),this.selector.style({visibility:"visible"}),this.update()},n.Dashboard.prototype.update=function(){return this.selector?(this.components.forEach(function(t){t.update()}),this.position()):this},n.Dashboard.prototype.position=function(){if(!this.selector)return this;if("panel"===this.type){var t=this.parent.getPageOrigin(),e=(t.y+3.5).toString()+"px",a=t.x.toString()+"px",i=(this.parent.layout.width-4).toString()+"px";this.selector.style({position:"absolute",top:e,left:a,width:i})}return this.components.forEach(function(t){t.position()}),this},n.Dashboard.prototype.hide=function(){return!this.selector||this.shouldPersist()?this:(this.components.forEach(function(t){t.hide()}),this.selector.style({visibility:"hidden"}),this)},n.Dashboard.prototype.destroy=function(t){return"undefined"==typeof t&&(t=!1),this.selector?this.shouldPersist()&&!t?this:(this.components.forEach(function(t){t.destroy(!0)}),this.components=[],this.selector.remove(),this.selector=null,this):this},n.Dashboard.Component=function(t,e){return this.layout=t||{},this.layout.color||(this.layout.color="gray"),this.parent=e||null,this.parent_panel=null,this.parent_plot=null,this.parent_svg=null,this.parent instanceof n.Dashboard&&("panel"===this.parent.type?(this.parent_panel=this.parent.parent,this.parent_plot=this.parent.parent.parent,this.parent_svg=this.parent_panel):(this.parent_plot=this.parent.parent,this.parent_svg=this.parent_plot)),this.selector=null,this.button=null,this.persist=!1,this.layout.position||(this.layout.position="left"),this},n.Dashboard.Component.prototype.show=function(){if(this.parent&&this.parent.selector){if(!this.selector){var t=["start","middle","end"].indexOf(this.layout.group_position)!==-1?" lz-dashboard-group-"+this.layout.group_position:"";this.selector=this.parent.selector.append("div").attr("class","lz-dashboard-"+this.layout.position+t),this.layout.style&&this.selector.style(this.layout.style),"function"==typeof this.initialize&&this.initialize()}return this.button&&"highlighted"===this.button.status&&this.button.menu.show(),this.selector.style({visibility:"visible"}),this.update(),this.position()}},n.Dashboard.Component.prototype.update=function(){},n.Dashboard.Component.prototype.position=function(){return this.button&&this.button.menu.position(),this},n.Dashboard.Component.prototype.shouldPersist=function(){return!!this.persist||!(!this.button||!this.button.persist)},n.Dashboard.Component.prototype.hide=function(){return!this.selector||this.shouldPersist()?this:(this.button&&this.button.menu.hide(),this.selector.style({visibility:"hidden"}),this)},n.Dashboard.Component.prototype.destroy=function(t){return"undefined"==typeof t&&(t=!1),this.selector?this.shouldPersist()&&!t?this:(this.button&&this.button.menu&&this.button.menu.destroy(),this.selector.remove(),this.selector=null,this.button=null,this):this},n.Dashboard.Components=function(){var t={},e={};return t.get=function(t,a,i){if(t){if(e[t]){if("object"!=typeof a)throw"invalid layout argument for dashboard component ["+t+"]";return new e[t](a,i)}throw"dashboard component ["+t+"] not found"}return null},t.set=function(t,a){if(a){if("function"!=typeof a)throw"unable to set dashboard component ["+t+"], argument provided is not a function";e[t]=a,e[t].prototype=new n.Dashboard.Component}else delete e[t]},t.add=function(a,i){if(e[a])throw"dashboard component already exists with name: "+a;t.set(a,i)},t.list=function(){return Object.keys(e)},t}(),n.Dashboard.Component.Button=function(e){if(!(e instanceof n.Dashboard.Component))throw"Unable to create dashboard component button, invalid parent";this.parent=e,this.parent_panel=this.parent.parent_panel,this.parent_plot=this.parent.parent_plot,this.parent_svg=this.parent.parent_svg,this.parent_dashboard=this.parent.parent,this.selector=null,this.tag="a",this.setTag=function(t){return"undefined"!=typeof t&&(this.tag=t.toString()),this},this.html="",this.setHtml=function(t){return"undefined"!=typeof t&&(this.html=t.toString()),this},this.setText=this.setHTML,this.title="",this.setTitle=function(t){return"undefined"!=typeof t&&(this.title=t.toString()),this},this.color="gray",this.setColor=function(t){return"undefined"!=typeof t&&(["gray","red","orange","yellow","green","blue","purple"].indexOf(t)!==-1?this.color=t:this.color="gray"),this},this.style={},this.setStyle=function(t){return"undefined"!=typeof t&&(this.style=t),this},this.getClass=function(){var t=["start","middle","end"].indexOf(this.parent.layout.group_position)!==-1?" lz-dashboard-button-group-"+this.parent.layout.group_position:"";return"lz-dashboard-button lz-dashboard-button-"+this.color+(this.status?"-"+this.status:"")+t},this.persist=!1,this.permanent=!1,this.setPermanent=function(t){return t="undefined"==typeof t||Boolean(t),this.permanent=t,this.permanent&&(this.persist=!0),this},this.shouldPersist=function(){return this.permanent||this.persist},this.status="",this.setStatus=function(t){return"undefined"!=typeof t&&["","highlighted","disabled"].indexOf(t)!==-1&&(this.status=t),this.update()},this.highlight=function(t){return t="undefined"==typeof t||Boolean(t),t?this.setStatus("highlighted"):"highlighted"===this.status?this.setStatus(""):this},this.disable=function(t){return t="undefined"==typeof t||Boolean(t),t?this.setStatus("disabled"):"disabled"===this.status?this.setStatus(""):this},this.onmouseover=function(){},this.setOnMouseover=function(t){return"function"==typeof t?this.onmouseover=t:this.onmouseover=function(){},this},this.onmouseout=function(){},this.setOnMouseout=function(t){return"function"==typeof t?this.onmouseout=t:this.onmouseout=function(){},this},this.onclick=function(){},this.setOnclick=function(t){return"function"==typeof t?this.onclick=t:this.onclick=function(){},this},this.show=function(){if(this.parent)return this.selector||(this.selector=this.parent.selector.append(this.tag).attr("class",this.getClass())),this.update()},this.preUpdate=function(){return this},this.update=function(){return this.selector?(this.preUpdate(),this.selector.attr("class",this.getClass()).attr("title",this.title).style(this.style).on("mouseover","disabled"===this.status?null:this.onmouseover).on("mouseout","disabled"===this.status?null:this.onmouseout).on("click","disabled"===this.status?null:this.onclick).html(this.html),this.menu.update(),this.postUpdate(),this):this},this.postUpdate=function(){return this},this.hide=function(){return this.selector&&!this.shouldPersist()&&(this.selector.remove(),this.selector=null),this},this.menu={outer_selector:null,inner_selector:null,scroll_position:0,hidden:!0,show:function(){return this.menu.outer_selector||(this.menu.outer_selector=t.select(this.parent_plot.svg.node().parentNode).append("div").attr("class","lz-dashboard-menu lz-dashboard-menu-"+this.color).attr("id",this.parent_svg.getBaseId()+".dashboard.menu"),this.menu.inner_selector=this.menu.outer_selector.append("div").attr("class","lz-dashboard-menu-content"),this.menu.inner_selector.on("scroll",function(){this.menu.scroll_position=this.menu.inner_selector.node().scrollTop}.bind(this))),this.menu.outer_selector.style({visibility:"visible"}),this.menu.hidden=!1,this.menu.update()}.bind(this),update:function(){return this.menu.outer_selector?(this.menu.populate(),this.menu.inner_selector&&(this.menu.inner_selector.node().scrollTop=this.menu.scroll_position),this.menu.position()):this.menu}.bind(this),position:function(){if(!this.menu.outer_selector)return this.menu;this.menu.outer_selector.style({height:null});var t=3,e=20,a=14,i=this.parent_svg.getPageOrigin(),n=document.documentElement.scrollTop||document.body.scrollTop,s=this.parent_plot.getContainerOffset(),o=this.parent_dashboard.selector.node().getBoundingClientRect(),r=this.selector.node().getBoundingClientRect(),l=this.menu.outer_selector.node().getBoundingClientRect(),h=this.menu.inner_selector.node().scrollHeight,d=0,u=0;"panel"===this.parent_dashboard.type?(d=i.y+o.height+2*t,u=Math.max(i.x+this.parent_svg.layout.width-l.width-t,i.x+t)):(d=r.bottom+n+t-s.top,u=Math.max(r.left+r.width-l.width-s.left,i.x+t));var p=Math.max(this.parent_svg.layout.width-2*t-e,e),c=p,y=p-4*t,f=Math.max(this.parent_svg.layout.height-10*t-a,a),g=Math.min(h,f),_=f;return this.menu.outer_selector.style({top:d.toString()+"px",left:u.toString()+"px","max-width":c.toString()+"px","max-height":_.toString()+"px",height:g.toString()+"px"}),this.menu.inner_selector.style({"max-width":y.toString()+"px"}),this.menu.inner_selector.node().scrollTop=this.menu.scroll_position,this.menu}.bind(this),hide:function(){return this.menu.outer_selector?(this.menu.outer_selector.style({visibility:"hidden"}),this.menu.hidden=!0,this.menu):this.menu}.bind(this),destroy:function(){return this.menu.outer_selector?(this.menu.inner_selector.remove(),this.menu.outer_selector.remove(),this.menu.inner_selector=null,this.menu.outer_selector=null,this.menu):this.menu}.bind(this),populate:function(){}.bind(this),setPopulate:function(t){return"function"==typeof t?(this.menu.populate=t,this.setOnclick(function(){this.menu.hidden?(this.menu.show(),this.highlight().update(),this.persist=!0):(this.menu.hide(),this.highlight(!1).update(),this.permanent||(this.persist=!1))}.bind(this))):this.setOnclick(),this}.bind(this)}},n.Dashboard.Components.add("title",function(t){n.Dashboard.Component.apply(this,arguments),this.show=function(){return this.div_selector=this.parent.selector.append("div").attr("class","lz-dashboard-title lz-dashboard-"+this.layout.position),this.title_selector=this.div_selector.append("h3"),this.update()},this.update=function(){var e=t.title.toString();return this.layout.subtitle&&(e+=" "+this.layout.subtitle+""),this.title_selector.html(e),this}}),n.Dashboard.Components.add("dimensions",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){var e=this.parent_plot.layout.width.toString().indexOf(".")===-1?this.parent_plot.layout.width:this.parent_plot.layout.width.toFixed(2),a=this.parent_plot.layout.height.toString().indexOf(".")===-1?this.parent_plot.layout.height:this.parent_plot.layout.height.toFixed(2);return this.selector.html(e+"px × "+a+"px"),t.class&&this.selector.attr("class",t.class),t.style&&this.selector.style(t.style),this}}),n.Dashboard.Components.add("region_scale",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)||null===this.parent_plot.state.start||null===this.parent_plot.state.end?this.selector.style("display","none"):(this.selector.style("display",null),this.selector.html(n.positionIntToString(this.parent_plot.state.end-this.parent_plot.state.start,null,!0))),t.class&&this.selector.attr("class",t.class),t.style&&this.selector.style(t.style),this}}),n.Dashboard.Components.add("download",function(a){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(a.color).setHtml("Download Image").setTitle("Download image of the current plot as locuszoom.svg").setOnMouseover(function(){this.button.selector.classed("lz-dashboard-button-gray-disabled",!0).html("Preparing Image"),this.generateBase64SVG().then(function(t){this.button.selector.attr("href","data:image/svg+xml;base64,\n"+t).classed("lz-dashboard-button-gray-disabled",!1).classed("lz-dashboard-button-gray-highlighted",!0).html("Download Image")}.bind(this))}.bind(this)).setOnMouseout(function(){this.button.selector.classed("lz-dashboard-button-gray-highlighted",!1)}.bind(this)),this.button.show(),this.button.selector.attr("href-lang","image/svg+xml").attr("download","locuszoom.svg"),this)},this.css_string="";for(var i in Object.keys(document.styleSheets))if(null!==document.styleSheets[i].href&&document.styleSheets[i].href.indexOf("locuszoom.css")!==-1){n.createCORSPromise("GET",document.styleSheets[i].href).then(function(t){this.css_string=t.replace(/[\r\n]/g," ").replace(/\s+/g," "),this.css_string.indexOf("/* ! LocusZoom HTML Styles */")&&(this.css_string=this.css_string.substring(0,this.css_string.indexOf("/* ! LocusZoom HTML Styles */")))}.bind(this));break}this.generateBase64SVG=function(){return e.fcall(function(){var e=this.parent.selector.append("div").style("display","none").html(this.parent_plot.svg.node().outerHTML);e.selectAll("g.lz-curtain").remove(),e.selectAll("g.lz-mouse_guide").remove(),e.selectAll("g.tick text").each(function(){var e=10*+t.select(this).attr("dy").substring(-2).slice(0,-2);t.select(this).attr("dy",e)});var a=t.select(e.select("svg").node().parentNode).html(),i='",n=a.indexOf(">")+1;return a=a.slice(0,n)+i+a.slice(n),e.remove(),btoa(encodeURIComponent(a).replace(/%([0-9A-F]{2})/g,function(t,e){return String.fromCharCode("0x"+e)}))}.bind(this))}}),n.Dashboard.Components.add("remove_panel",function(e){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(e.color).setHtml("×").setTitle("Remove panel").setOnclick(function(){if(!e.suppress_confirm&&!confirm("Are you sure you want to remove this panel? This cannot be undone!"))return!1;var a=this.parent_panel;return a.dashboard.hide(!0),t.select(a.parent.svg.node().parentNode).on("mouseover."+a.getBaseId()+".dashboard",null),t.select(a.parent.svg.node().parentNode).on("mouseout."+a.getBaseId()+".dashboard",null),a.parent.removePanel(a.id)}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("move_panel_up",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){if(this.button){var e=0===this.parent_panel.layout.y_index;return this.button.disable(e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("▴").setTitle("Move panel up").setOnclick(function(){this.parent_panel.moveUp(),this.update()}.bind(this)),this.button.show(),this.update()}}),n.Dashboard.Components.add("move_panel_down",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){if(this.button){var e=this.parent_panel.layout.y_index===this.parent_plot.panel_ids_by_y_index.length-1;return this.button.disable(e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("▾").setTitle("Move panel down").setOnclick(function(){this.parent_panel.moveDown(),this.update()}.bind(this)),this.button.show(),this.update()}}),n.Dashboard.Components.add("shift_region",function(t){return n.Dashboard.Component.apply(this,arguments),isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)?(this.update=function(){},void console.warn("Unable to add shift_region dashboard component: plot state does not have region bounds")):((isNaN(t.step)||0===t.step)&&(t.step=5e4),"string"!=typeof t.button_html&&(t.button_html=t.step>0?">":"<"),"string"!=typeof t.button_title&&(t.button_title="Shift region by "+(t.step>0?"+":"-")+n.positionIntToString(Math.abs(t.step),null,!0)),void(this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){this.parent_plot.applyState({start:Math.max(this.parent_plot.state.start+t.step,1),end:this.parent_plot.state.end+t.step})}.bind(this)),this.button.show(),this)}))}),n.Dashboard.Components.add("zoom_region",function(t){return n.Dashboard.Component.apply(this,arguments),isNaN(this.parent_plot.state.start)||isNaN(this.parent_plot.state.end)?(this.update=function(){},void console.warn("Unable to add zoom_region dashboard component: plot state does not have region bounds")):((isNaN(t.step)||0===t.step)&&(t.step=.2),"string"!=typeof t.button_html&&(t.button_html=t.step>0?"z–":"z+"),"string"!=typeof t.button_title&&(t.button_title="Zoom region "+(t.step>0?"out":"in")+" by "+(100*Math.abs(t.step)).toFixed(1)+"%"),void(this.update=function(){if(this.button){var e=!0,a=this.parent_plot.state.end-this.parent_plot.state.start;return t.step>0&&!isNaN(this.parent_plot.layout.max_region_scale)&&a>=this.parent_plot.layout.max_region_scale&&(e=!1),t.step<0&&!isNaN(this.parent_plot.layout.min_region_scale)&&a<=this.parent_plot.layout.min_region_scale&&(e=!1),this.button.disable(!e),this}return this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){var e=this.parent_plot.state.end-this.parent_plot.state.start,a=1+t.step,i=e*a;isNaN(this.parent_plot.layout.max_region_scale)||(i=Math.min(i,this.parent_plot.layout.max_region_scale)),isNaN(this.parent_plot.layout.min_region_scale)||(i=Math.max(i,this.parent_plot.layout.min_region_scale));var n=Math.floor((i-e)/2);this.parent_plot.applyState({start:Math.max(this.parent_plot.state.start-n,1),end:this.parent_plot.state.end+n})}.bind(this)),this.button.show(),this}))}),n.Dashboard.Components.add("menu",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title),this.button.menu.setPopulate(function(){this.button.menu.inner_selector.html(t.menu_html)}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("covariates_model",function(t){n.Dashboard.Component.apply(this,arguments),this.initialize=function(){this.parent_plot.state.model=this.parent_plot.state.model||{},this.parent_plot.state.model.covariates=this.parent_plot.state.model.covariates||[],this.parent_plot.CovariatesModel={button:this,add:function(t){var e=JSON.parse(JSON.stringify(t));"object"==typeof t&&"string"!=typeof e.html&&(e.html="function"==typeof t.toHTML?t.toHTML():t.toString());for(var a=0;a1?"covariates":"covariate";t+=" ("+this.parent_plot.state.model.covariates.length+" "+e+")"}this.button.setHtml(t).disable(!1)}.bind(this),this.button.show(),this)}}),n.Dashboard.Components.add("toggle_split_tracks",function(t){if(n.Dashboard.Component.apply(this,arguments),t.data_layer_id||(t.data_layer_id="intervals"),!this.parent_panel.data_layers[t.data_layer_id])throw"Dashboard toggle split tracks component missing valid data layer ID";this.update=function(){var e=this.parent_panel.data_layers[t.data_layer_id],a=e.layout.split_tracks?"Merge Tracks":"Split Tracks";return this.button?(this.button.setHtml(a),this.button.show(),this.parent.position(),this):(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(a).setTitle("Toggle whether tracks are split apart or merged together").setOnclick(function(){e.toggleSplitTracks(),this.scale_timeout&&clearTimeout(this.scale_timeout);var t=e.layout.transition?+e.layout.transition.duration||0:0;this.scale_timeout=setTimeout(function(){this.parent_panel.scaleHeightToData(),this.parent_plot.positionPanels()}.bind(this),t),this.update()}.bind(this)),this.update())}}),n.Dashboard.Components.add("resize_to_data",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml("Resize to Data").setTitle("Automatically resize this panel to fit the data its currently showing").setOnclick(function(){this.parent_panel.scaleHeightToData(),this.update()}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("toggle_legend",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){var e=this.parent_panel.legend.layout.hidden?"Show Legend":"Hide Legend";return this.button?(this.button.setHtml(e).show(),this.parent.position(),this):(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setTitle("Show or hide the legend for this panel").setOnclick(function(){this.parent_panel.legend.layout.hidden=!this.parent_panel.legend.layout.hidden,this.parent_panel.legend.render(),this.update()}.bind(this)),this.update())}}),n.Dashboard.Components.add("data_layers",function(t){n.Dashboard.Component.apply(this,arguments),this.update=function(){return"string"!=typeof t.button_html&&(t.button_html="Data Layers"),"string"!=typeof t.button_title&&(t.button_title="Manipulate Data Layers (sort, dim, show/hide, etc.)"),this.button?this:(this.button=new n.Dashboard.Component.Button(this).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){this.button.menu.populate()}.bind(this)),this.button.menu.setPopulate(function(){this.button.menu.inner_selector.html("");var e=this.button.menu.inner_selector.append("table");return this.parent_panel.data_layer_ids_by_z_index.slice().reverse().forEach(function(a,i){var s=this.parent_panel.data_layers[a],o="string"!=typeof s.layout.name?s.id:s.layout.name,r=e.append("tr");r.append("td").html(o),t.statuses.forEach(function(t){var e,a,i,o=n.DataLayer.Statuses.adjectives.indexOf(t),l=n.DataLayer.Statuses.verbs[o];s.global_statuses[t]?(e=n.DataLayer.Statuses.menu_antiverbs[o],a="un"+l+"AllElements",i="-highlighted"):(e=n.DataLayer.Statuses.verbs[o],a=l+"AllElements",i=""),r.append("td").append("a").attr("class","lz-dashboard-button lz-dashboard-button-"+this.layout.color+i).style({"margin-left":"0em"}).on("click",function(){s[a](),this.button.menu.populate()}.bind(this)).html(e)}.bind(this));var l=0===i,h=i===this.parent_panel.data_layer_ids_by_z_index.length-1,d=r.append("td");d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-start lz-dashboard-button-"+this.layout.color+(h?"-disabled":"")).style({"margin-left":"0em"}).on("click",function(){s.moveDown(),this.button.menu.populate()}.bind(this)).html("▾").attr("title","Move layer down (further back)"),d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-middle lz-dashboard-button-"+this.layout.color+(l?"-disabled":"")).style({"margin-left":"0em"}).on("click",function(){s.moveUp(),this.button.menu.populate()}.bind(this)).html("▴").attr("title","Move layer up (further front)"),d.append("a").attr("class","lz-dashboard-button lz-dashboard-button-group-end lz-dashboard-button-red").style({"margin-left":"0em"}).on("click",function(){return confirm("Are you sure you want to remove the "+o+" layer? This cannot be undone!")&&s.parent.removeDataLayer(a),this.button.menu.populate()}.bind(this)).html("×").attr("title","Remove layer")}.bind(this)),this}.bind(this)),this.button.show(),this)}}),n.Dashboard.Components.add("display_options",function(t){"string"!=typeof t.button_html&&(t.button_html="Display options"),"string"!=typeof t.button_title&&(t.button_title="Control how plot items are displayed"),n.Dashboard.Component.apply(this,arguments);var e=t.fields_whitelist||["color","fill_opacity","label","legend","point_shape","point_size","tooltip","tooltip_positioning"],a=this.parent_panel.data_layers[t.layer_name],i=a.layout,s={};e.forEach(function(t){var e=i[t];e&&(s[t]=JSON.parse(JSON.stringify(e)))}),this._selected_item="default";var o=this;this.button=new n.Dashboard.Component.Button(o).setColor(t.color).setHtml(t.button_html).setTitle(t.button_title).setOnclick(function(){o.button.menu.populate()}),this.button.menu.setPopulate(function(){var t=Math.floor(1e4*Math.random()).toString();o.button.menu.inner_selector.html("");var e=o.button.menu.inner_selector.append("table"),i=o.layout,n=function(i,n,s){var r=e.append("tr");r.append("td").append("input").attr({type:"radio",name:"color-picker-"+t,value:s}).property("checked",s===o._selected_item).on("click",function(){Object.keys(n).forEach(function(t){a.layout[t]=n[t]}),o._selected_item=s,o.parent_panel.render();var t=o.parent_panel.legend;t&&n.legend&&t.render()}),r.append("td").text(i)},r=i.default_config_display_name||"Default style";return n(r,s,"default"),i.options.forEach(function(t,e){n(t.display_name,t.display,e)}),o}),this.update=function(){return this.button.show(),this}}),n.Legend=function(t){if(!(t instanceof n.Panel))throw"Unable to create legend, parent must be a locuszoom panel";return this.parent=t,this.id=this.parent.getBaseId()+".legend",this.parent.layout.legend=n.Layouts.merge(this.parent.layout.legend||{},n.Legend.DefaultLayout),this.layout=this.parent.layout.legend,this.selector=null,this.background_rect=null,this.elements=[],this.elements_group=null,this.hidden=!1,this.render()},n.Legend.DefaultLayout={orientation:"vertical",origin:{x:0,y:0},width:10,height:10,padding:5,label_size:12,hidden:!1},n.Legend.prototype.render=function(){this.selector||(this.selector=this.parent.svg.group.append("g").attr("id",this.parent.getBaseId()+".legend").attr("class","lz-legend")),this.background_rect||(this.background_rect=this.selector.append("rect").attr("width",100).attr("height",100).attr("class","lz-legend-background")),this.elements_group||(this.elements_group=this.selector.append("g")),this.elements.forEach(function(t){t.remove()}),this.elements=[];var e=+this.layout.padding||1,a=e,i=e,n=0;this.parent.data_layer_ids_by_z_index.slice().reverse().forEach(function(s){Array.isArray(this.parent.data_layers[s].layout.legend)&&this.parent.data_layers[s].layout.legend.forEach(function(s){var o=this.elements_group.append("g").attr("transform","translate("+a+","+i+")"),r=+s.label_size||+this.layout.label_size||12,l=0,h=r/2+e/2;if(n=Math.max(n,r+e),"line"===s.shape){var d=+s.length||16,u=r/4+e/2;o.append("path").attr("class",s.class||"").attr("d","M0,"+u+"L"+d+","+u).style(s.style||{}),l=d+e}else if("rect"===s.shape){var p=+s.width||16,c=+s.height||p;o.append("rect").attr("class",s.class||"").attr("width",p).attr("height",c).attr("fill",s.color||{}).style(s.style||{}),l=p+e,n=Math.max(n,c+e)}else if(t.svg.symbolTypes.indexOf(s.shape)!==-1){var y=+s.size||40,f=Math.ceil(Math.sqrt(y/Math.PI));o.append("path").attr("class",s.class||"").attr("d",t.svg.symbol().size(y).type(s.shape)).attr("transform","translate("+f+","+(f+e/2)+")").attr("fill",s.color||{}).style(s.style||{}),l=2*f+e,h=Math.max(2*f+e/2,h),n=Math.max(n,2*f+e)}o.append("text").attr("text-anchor","left").attr("class","lz-label").attr("x",l).attr("y",h).style({"font-size":r}).text(s.label);var g=o.node().getBoundingClientRect();if("vertical"===this.layout.orientation)i+=g.height+e,n=0;else{var _=this.layout.origin.x+a+g.width;a>e&&_>this.parent.layout.width&&(i+=n,a=e,o.attr("transform","translate("+a+","+i+")")),a+=g.width+3*e}this.elements.push(o)}.bind(this))}.bind(this));var s=this.elements_group.node().getBoundingClientRect();return this.layout.width=s.width+2*this.layout.padding,this.layout.height=s.height+2*this.layout.padding,this.background_rect.attr("width",this.layout.width).attr("height",this.layout.height),this.selector.style({visibility:this.layout.hidden?"hidden":"visible"}),this.position()},n.Legend.prototype.position=function(){if(!this.selector)return this;var t=this.selector.node().getBoundingClientRect();isNaN(+this.layout.pad_from_bottom)||(this.layout.origin.y=this.parent.layout.height-t.height-+this.layout.pad_from_bottom),isNaN(+this.layout.pad_from_right)||(this.layout.origin.x=this.parent.layout.width-t.width-+this.layout.pad_from_right),this.selector.attr("transform","translate("+this.layout.origin.x+","+this.layout.origin.y+")")},n.Legend.prototype.hide=function(){this.layout.hidden=!0,this.render()},n.Legend.prototype.show=function(){this.layout.hidden=!1,this.render()},n.Data=n.Data||{},n.DataSources=function(){this.sources={}},n.DataSources.prototype.addSource=function(t,e){return console.warn("Warning: .addSource() is deprecated. Use .add() instead"),this.add(t,e)},n.DataSources.prototype.add=function(t,e){return this.set(t,e)},n.DataSources.prototype.set=function(t,e){if(Array.isArray(e)){var a=n.KnownDataSources.create.apply(null,e);this.sources[t]=a}else null!==e?this.sources[t]=e:delete this.sources[t];return this},n.DataSources.prototype.getSource=function(t){return console.warn("Warning: .getSource() is deprecated. Use .get() instead"),this.get(t)},n.DataSources.prototype.get=function(t){return this.sources[t]},n.DataSources.prototype.removeSource=function(t){return console.warn("Warning: .removeSource() is deprecated. Use .remove() instead"),this.remove(t)},n.DataSources.prototype.remove=function(t){return this.set(t,null)},n.DataSources.prototype.fromJSON=function(t){"string"==typeof t&&(t=JSON.parse(t));var e=this;return Object.keys(t).forEach(function(a){e.set(a,t[a])}),e},n.DataSources.prototype.keys=function(){return Object.keys(this.sources)},n.DataSources.prototype.toJSON=function(){return this.sources},n.Data.Field=function(t){var e=/^(?:([^:]+):)?([^:|]*)(\|.+)*$/.exec(t);this.full_name=t,this.namespace=e[1]||null,this.name=e[2]||null,this.transformations=[],"string"==typeof e[3]&&e[3].length>1&&(this.transformations=e[3].substring(1).split("|"),this.transformations.forEach(function(t,e){this.transformations[e]=n.TransformationFunctions.get(t)}.bind(this))),this.applyTransformations=function(t){return this.transformations.forEach(function(e){t=e(t)}),t},this.resolve=function(t){if("undefined"==typeof t[this.full_name]){var e=null;"undefined"!=typeof t[this.namespace+":"+this.name]?e=t[this.namespace+":"+this.name]:"undefined"!=typeof t[this.name]&&(e=t[this.name]),t[this.full_name]=this.applyTransformations(e)}return t[this.full_name]; +}},n.Data.Requester=function(t){function a(t){var e={},a=/^(?:([^:]+):)?([^:|]*)(\|.+)*$/;return t.forEach(function(t){var i=a.exec(t),s=i[1]||"base",o=i[2],r=n.TransformationFunctions.get(i[3]);"undefined"==typeof e[s]&&(e[s]={outnames:[],fields:[],trans:[]}),e[s].outnames.push(t),e[s].fields.push(o),e[s].trans.push(r)}),e}this.getData=function(i,n){for(var s=a(n),o=Object.keys(s).map(function(e){if(!t.get(e))throw"Datasource for namespace "+e+" not found";return t.get(e).getData(i,s[e].fields,s[e].outnames,s[e].trans)}),r=e.when({header:{},body:{}}),l=0;l1&&(2!==e.length||e.indexOf("isrefvar")===-1))throw"LD does not know how to get all fields: "+e.join(", ")},n.Data.LDSource.prototype.findMergeFields=function(t){var e=function(t){return function(){for(var e=arguments,a=0;a0){var i=Object.keys(t.body[0]),n=e(i);a.id=a.id||n(/\bvariant\b/)||n(/\bid\b/),a.position=a.position||n(/\bposition\b/i,/\bpos\b/i),a.pvalue=a.pvalue||n(/\bpvalue\b/i,/\blog_pvalue\b/i),a._names_=i}return a},n.Data.LDSource.prototype.findRequestedFields=function(t,e){for(var a={},i=0;ii&&(i=t[s][e]*a,n=s);return n},n=t.ldrefsource||e.header.ldrefsource||1,s=this.findRequestedFields(a),o=s.ldin;if("state"===o&&(o=t.ldrefvar||e.header.ldrefvar||"best"),"best"===o){if(!e.body)throw"No association data found to find best pvalue";var r=this.findMergeFields(e);if(!r.pvalue||!r.id){var l="";throw r.id||(l+=(l.length?", ":"")+"id"),r.pvalue||(l+=(l.length?", ":"")+"pvalue"),"Unable to find necessary column(s) for merge: "+l+" (available: "+r._names_+")"}o=e.body[i(e.body,r.pvalue)][r.id]}return e.header||(e.header={}),e.header.ldrefvar=o,this.url+"results/?filter=reference eq "+n+" and chromosome2 eq '"+t.chr+"' and position2 ge "+t.start+" and position2 le "+t.end+" and variant1 eq '"+o+"'&fields=chr,pos,rsquare"},n.Data.LDSource.prototype.parseResponse=function(t,e,a,i){var n=JSON.parse(t),s=this.findMergeFields(e),o=this.findRequestedFields(a,i);if(!s.position)throw"Unable to find position field for merge: "+s._names_;var r=function(t,e,a,i){for(var n=0,o=0;n0&&parseFloat(this.panels[i].layout.proportional_height)>0&&(s=Math.max(s,this.panels[i].layout.min_height/this.panels[i].layout.proportional_height));if(this.layout.min_width=Math.max(n,1),this.layout.min_height=Math.max(s,1),t.select(this.svg.node().parentNode).style({"min-width":this.layout.min_width+"px","min-height":this.layout.min_height+"px"}),!isNaN(e)&&e>=0&&!isNaN(a)&&a>=0){this.layout.width=Math.max(Math.round(+e),this.layout.min_width),this.layout.height=Math.max(Math.round(+a),this.layout.min_height),this.layout.aspect_ratio=this.layout.width/this.layout.height,this.layout.responsive_resize&&(this.svg&&(this.layout.width=Math.max(this.svg.node().parentNode.getBoundingClientRect().width,this.layout.min_width)),this.layout.height=this.layout.width/this.layout.aspect_ratio,this.layout.height0)e.layout.y_index<0&&(e.layout.y_index=Math.max(this.panel_ids_by_y_index.length+e.layout.y_index,0)),this.panel_ids_by_y_index.splice(e.layout.y_index,0,e.id),this.applyPanelYIndexesToPanelLayouts();else{var a=this.panel_ids_by_y_index.push(e.id);this.panels[e.id].layout.y_index=a-1}var i=null;return this.layout.panels.forEach(function(t,a){t.id===e.id&&(i=a)}),null===i&&(i=this.layout.panels.push(this.panels[e.id].layout)-1),this.panels[e.id].layout_idx=i,this.initialized&&(this.positionPanels(),this.panels[e.id].initialize(),this.panels[e.id].reMap(),this.setDimensions(this.layout.width,this.layout.height)),this.panels[e.id]},n.Plot.prototype.clearPanelData=function(t,e){e=e||"wipe";var a;a=t?[t]:Object.keys(this.panels);var i=this;return a.forEach(function(t){i.panels[t].data_layer_ids_by_z_index.forEach(function(a){var n=i.panels[t].data_layers[a];n.destroyAllTooltips(),delete i.layout.state[t+"."+a],"reset"===e&&n.setDefaultState()})}),this},n.Plot.prototype.removePanel=function(t){if(!this.panels[t])throw"Unable to remove panel, ID not found: "+t;return this.panel_boundaries.hide(),this.clearPanelData(t),this.panels[t].loader.hide(),this.panels[t].dashboard.destroy(!0),this.panels[t].curtain.hide(),this.panels[t].svg.container&&this.panels[t].svg.container.remove(),this.layout.panels.splice(this.panels[t].layout_idx,1),delete this.panels[t],delete this.layout.state[t],this.layout.panels.forEach(function(t,e){this.panels[t.id].layout_idx=e}.bind(this)),this.panel_ids_by_y_index.splice(this.panel_ids_by_y_index.indexOf(t),1),this.applyPanelYIndexesToPanelLayouts(),this.initialized&&(this.layout.min_height=this._base_layout.min_height,this.layout.min_width=this._base_layout.min_width,this.positionPanels(),this.setDimensions(this.layout.width,this.layout.height)),this},n.Plot.prototype.positionPanels=function(){var t,e={left:0,right:0};for(t in this.panels)null===this.panels[t].layout.proportional_height&&(this.panels[t].layout.proportional_height=this.panels[t].layout.height/this.layout.height),null===this.panels[t].layout.proportional_width&&(this.panels[t].layout.proportional_width=1),this.panels[t].layout.interaction.x_linked&&(e.left=Math.max(e.left,this.panels[t].layout.margin.left),e.right=Math.max(e.right,this.panels[t].layout.margin.right));var a=this.sumProportional("height");if(!a)return this;var i=1/a;for(t in this.panels)this.panels[t].layout.proportional_height*=i;var n=0;this.panel_ids_by_y_index.forEach(function(t){if(this.panels[t].setOrigin(0,n),this.panels[t].layout.proportional_origin.x=0,n+=this.panels[t].layout.height,this.panels[t].layout.interaction.x_linked){var a=Math.max(e.left-this.panels[t].layout.margin.left,0)+Math.max(e.right-this.panels[t].layout.margin.right,0);this.panels[t].layout.width+=a,this.panels[t].layout.margin.left=e.left,this.panels[t].layout.margin.right=e.right,this.panels[t].layout.cliparea.origin.x=e.left}}.bind(this));var s=n;return this.panel_ids_by_y_index.forEach(function(t){this.panels[t].layout.proportional_origin.y=this.panels[t].layout.origin.y/s}.bind(this)),this.setDimensions(),this.panel_ids_by_y_index.forEach(function(t){this.panels[t].setDimensions(this.layout.width*this.panels[t].layout.proportional_width,this.layout.height*this.panels[t].layout.proportional_height)}.bind(this)),this},n.Plot.prototype.initialize=function(){if(this.layout.responsive_resize&&t.select(this.container).classed("lz-container-responsive",!0),this.layout.mouse_guide){var e=this.svg.append("g").attr("class","lz-mouse_guide").attr("id",this.id+".mouse_guide"),a=e.append("rect").attr("class","lz-mouse_guide-vertical").attr("x",-1),i=e.append("rect").attr("class","lz-mouse_guide-horizontal").attr("y",-1);this.mouse_guide={svg:e,vertical:a,horizontal:i}}this.curtain=n.generateCurtain.call(this),this.loader=n.generateLoader.call(this),this.panel_boundaries={parent:this,hide_timeout:null,showing:!1,dragging:!1,selectors:[],corner_selector:null,show:function(){if(!this.showing&&!this.parent.curtain.showing){this.showing=!0,this.parent.panel_ids_by_y_index.forEach(function(e,a){var i=t.select(this.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip").attr("class","lz-panel-boundary").attr("title","Resize panel");i.append("span");var n=t.behavior.drag();n.on("dragstart",function(){this.dragging=!0}.bind(this)),n.on("dragend",function(){this.dragging=!1}.bind(this)),n.on("drag",function(){var e=this.parent.panels[this.parent.panel_ids_by_y_index[a]],i=e.layout.height;e.setDimensions(e.layout.width,e.layout.height+t.event.dy);var n=e.layout.height-i,s=this.parent.layout.height+n;this.parent.panel_ids_by_y_index.forEach(function(t,e){var i=this.parent.panels[this.parent.panel_ids_by_y_index[e]];i.layout.proportional_height=i.layout.height/s,e>a&&(i.setOrigin(i.layout.origin.x,i.layout.origin.y+n),i.dashboard.position())}.bind(this)),this.parent.positionPanels(),this.position()}.bind(this)),i.call(n),this.parent.panel_boundaries.selectors.push(i)}.bind(this));var e=t.select(this.parent.svg.node().parentNode).insert("div",".lz-data_layer-tooltip").attr("class","lz-panel-corner-boundary").attr("title","Resize plot");e.append("span").attr("class","lz-panel-corner-boundary-outer"),e.append("span").attr("class","lz-panel-corner-boundary-inner");var a=t.behavior.drag();a.on("dragstart",function(){this.dragging=!0}.bind(this)),a.on("dragend",function(){this.dragging=!1}.bind(this)),a.on("drag",function(){this.setDimensions(this.layout.width+t.event.dx,this.layout.height+t.event.dy)}.bind(this.parent)),e.call(a),this.parent.panel_boundaries.corner_selector=e}return this.position()},position:function(){if(!this.showing)return this;var t=this.parent.getPageOrigin();this.selectors.forEach(function(e,a){var i=this.parent.panels[this.parent.panel_ids_by_y_index[a]].getPageOrigin(),n=t.x,s=i.y+this.parent.panels[this.parent.panel_ids_by_y_index[a]].layout.height-12,o=this.parent.layout.width-1;e.style({top:s+"px",left:n+"px",width:o+"px"}),e.select("span").style({width:o+"px"})}.bind(this));var e=10,a=16;return this.corner_selector.style({top:t.y+this.parent.layout.height-e-a+"px",left:t.x+this.parent.layout.width-e-a+"px"}),this},hide:function(){return this.showing?(this.showing=!1,this.selectors.forEach(function(t){t.remove()}),this.selectors=[],this.corner_selector.remove(),this.corner_selector=null,this):this}},this.layout.panel_boundaries&&(t.select(this.svg.node().parentNode).on("mouseover."+this.id+".panel_boundaries",function(){clearTimeout(this.panel_boundaries.hide_timeout),this.panel_boundaries.show()}.bind(this)),t.select(this.svg.node().parentNode).on("mouseout."+this.id+".panel_boundaries",function(){this.panel_boundaries.hide_timeout=setTimeout(function(){this.panel_boundaries.hide()}.bind(this),300)}.bind(this))),this.dashboard=new n.Dashboard(this).show();for(var s in this.panels)this.panels[s].initialize();var o="."+this.id;if(this.layout.mouse_guide){var r=function(){this.mouse_guide.vertical.attr("x",-1),this.mouse_guide.horizontal.attr("y",-1)}.bind(this),l=function(){var e=t.mouse(this.svg.node());this.mouse_guide.vertical.attr("x",e[0]),this.mouse_guide.horizontal.attr("y",e[1])}.bind(this);this.svg.on("mouseout"+o+"-mouse_guide",r).on("touchleave"+o+"-mouse_guide",r).on("mousemove"+o+"-mouse_guide",l)}var h=function(){this.stopDrag()}.bind(this),d=function(){if(this.interaction.dragging){var e=t.mouse(this.svg.node());t.event&&t.event.preventDefault(),this.interaction.dragging.dragged_x=e[0]-this.interaction.dragging.start_x,this.interaction.dragging.dragged_y=e[1]-this.interaction.dragging.start_y,this.panels[this.interaction.panel_id].render(),this.interaction.linked_panel_ids.forEach(function(t){this.panels[t].render()}.bind(this))}}.bind(this);this.svg.on("mouseup"+o,h).on("touchend"+o,h).on("mousemove"+o,d).on("touchmove"+o,d),t.select("body").empty()||t.select("body").on("mouseup"+o,h).on("touchend"+o,h),this.initialized=!0;var u=this.svg.node().getBoundingClientRect(),p=u.width?u.width:this.layout.width,c=u.height?u.height:this.layout.height;return this.setDimensions(p,c),this},n.Plot.prototype.refresh=function(){return this.applyState()},n.Plot.prototype.applyState=function(t){if(t=t||{},"object"!=typeof t)throw"LocusZoom.applyState only accepts an object; "+typeof t+" given";var a=JSON.parse(JSON.stringify(this.state));for(var i in t)a[i]=t[i];a=n.validateState(a,this.layout);for(i in a)this.state[i]=a[i];this.emit("data_requested"),this.remap_promises=[],this.loading_data=!0;for(var s in this.panels)this.remap_promises.push(this.panels[s].reMap());return e.all(this.remap_promises).catch(function(t){console.error(t),this.curtain.drop(t),this.loading_data=!1}.bind(this)).then(function(){this.dashboard.update(),this.panel_ids_by_y_index.forEach(function(t){var e=this.panels[t];e.dashboard.update(),e.data_layer_ids_by_z_index.forEach(function(e){var a=this.data_layers[e],i=t+"."+e;for(var n in this.state[i])this.state[i].hasOwnProperty(n)&&Array.isArray(this.state[i][n])&&this.state[i][n].forEach(function(t){try{this.setElementStatus(n,this.getElementById(t),!0)}catch(t){console.error("Unable to apply state: "+i+", "+n)}}.bind(a))}.bind(e))}.bind(this)),this.emit("layout_changed"),this.emit("data_rendered"),this.loading_data=!1}.bind(this))},n.Plot.prototype.startDrag=function(e,a){e=e||null,a=a||null;var i=null;switch(a){case"background":case"x_tick":i="x";break;case"y1_tick":i="y1";break;case"y2_tick":i="y2"}if(!(e instanceof n.Panel&&i&&this.canInteract()))return this.stopDrag();var s=t.mouse(this.svg.node());return this.interaction={panel_id:e.id,linked_panel_ids:e.getLinkedPanelIds(i),dragging:{method:a,start_x:s[0],start_y:s[1],dragged_x:0,dragged_y:0,axis:i}},this.svg.style("cursor","all-scroll"),this},n.Plot.prototype.stopDrag=function(){if(!this.interaction.dragging)return this;if("object"!=typeof this.panels[this.interaction.panel_id])return this.interaction={},this;var t=this.panels[this.interaction.panel_id],e=function(e,a,i){t.data_layer_ids_by_z_index.forEach(function(n){t.data_layers[n].layout[e+"_axis"].axis===a&&(t.data_layers[n].layout[e+"_axis"].floor=i[0],t.data_layers[n].layout[e+"_axis"].ceiling=i[1],delete t.data_layers[n].layout[e+"_axis"].lower_buffer,delete t.data_layers[n].layout[e+"_axis"].upper_buffer,delete t.data_layers[n].layout[e+"_axis"].min_extent,delete t.data_layers[n].layout[e+"_axis"].ticks)})};switch(this.interaction.dragging.method){case"background":case"x_tick":0!==this.interaction.dragging.dragged_x&&(e("x",1,t.x_extent),this.applyState({start:t.x_extent[0],end:t.x_extent[1]}));break;case"y1_tick":case"y2_tick":if(0!==this.interaction.dragging.dragged_y){var a=parseInt(this.interaction.dragging.method[1]);e("y",a,t["y"+a+"_extent"])}}return this.interaction={},this.svg.style("cursor",null),this},n.Panel=function(t,e){if("object"!=typeof t)throw"Unable to create panel, invalid layout";if(this.parent=e||null,this.parent_plot=e,"string"==typeof t.id&&t.id.length){if(this.parent&&"undefined"!=typeof this.parent.panels[t.id])throw"Cannot create panel with id ["+t.id+"]; panel with that id already exists"}else if(this.parent){var a=null,i=function(){a="p"+Math.floor(Math.random()*Math.pow(10,8)),null!=a&&"undefined"==typeof this.parent.panels[a]||(a=i())}.bind(this);t.id=a}else t.id="p"+Math.floor(Math.random()*Math.pow(10,8));return this.id=t.id,this.initialized=!1,this.layout_idx=null,this.svg={},this.layout=n.Layouts.merge(t||{},n.Panel.DefaultLayout),this.parent?(this.state=this.parent.state,this.state_id=this.id,this.state[this.state_id]=this.state[this.state_id]||{}):(this.state=null,this.state_id=null),this.data_layers={},this.data_layer_ids_by_z_index=[],this.applyDataLayerZIndexesToDataLayerLayouts=function(){this.data_layer_ids_by_z_index.forEach(function(t,e){this.data_layers[t].layout.z_index=e}.bind(this))}.bind(this),this.data_promises=[],this.x_scale=null,this.y1_scale=null,this.y2_scale=null,this.x_extent=null,this.y1_extent=null,this.y2_extent=null,this.x_ticks=[],this.y1_ticks=[],this.y2_ticks=[],this.zoom_timeout=null,this.getBaseId=function(){return this.parent.id+"."+this.id},this.event_hooks={layout_changed:[],data_requested:[],data_rendered:[],element_clicked:[]},this.on=function(t,e){if(!Array.isArray(this.event_hooks[t]))throw"Unable to register event hook, invalid event: "+t.toString();if("function"!=typeof e)throw"Unable to register event hook, invalid hook function passed";return this.event_hooks[t].push(e),this},this.emit=function(t,e){if(!Array.isArray(this.event_hooks[t]))throw"LocusZoom attempted to throw an invalid event: "+t.toString();return e=e||this,this.event_hooks[t].forEach(function(t){t.call(e)}),this},this.getPageOrigin=function(){var t=this.parent.getPageOrigin();return{x:t.x+this.layout.origin.x,y:t.y+this.layout.origin.y}},this.initializeLayout(),this},n.Panel.DefaultLayout={title:{text:"",style:{},x:10,y:22},y_index:null,width:0,height:0,origin:{x:0,y:null},min_width:1,min_height:1,proportional_width:null,proportional_height:null,proportional_origin:{x:0,y:null},margin:{top:0,right:0,bottom:0,left:0},background_click:"clear_selections",dashboard:{components:[]},cliparea:{height:0,width:0,origin:{x:0,y:0}},axes:{x:{},y1:{},y2:{}},legend:null,interaction:{drag_background_to_pan:!1,drag_x_ticks_to_scale:!1,drag_y1_ticks_to_scale:!1,drag_y2_ticks_to_scale:!1,scroll_to_zoom:!1,x_linked:!1,y1_linked:!1,y2_linked:!1},data_layers:[]},n.Panel.prototype.initializeLayout=function(){if(0===this.layout.width&&null===this.layout.proportional_width&&(this.layout.proportional_width=1),0===this.layout.height&&null===this.layout.proportional_height){var t=Object.keys(this.parent.panels).length;t>0?this.layout.proportional_height=1/t:this.layout.proportional_height=1}return this.setDimensions(),this.setOrigin(),this.setMargin(),this.x_range=[0,this.layout.cliparea.width],this.y1_range=[this.layout.cliparea.height,0],this.y2_range=[this.layout.cliparea.height,0],["x","y1","y2"].forEach(function(t){Object.keys(this.layout.axes[t]).length&&this.layout.axes[t].render!==!1?(this.layout.axes[t].render=!0,this.layout.axes[t].label=this.layout.axes[t].label||null,this.layout.axes[t].label_function=this.layout.axes[t].label_function||null):this.layout.axes[t].render=!1}.bind(this)),this.layout.data_layers.forEach(function(t){this.addDataLayer(t)}.bind(this)),this},n.Panel.prototype.setDimensions=function(t,e){return"undefined"!=typeof t&&"undefined"!=typeof e?!isNaN(t)&&t>=0&&!isNaN(e)&&e>=0&&(this.layout.width=Math.max(Math.round(+t),this.layout.min_width),this.layout.height=Math.max(Math.round(+e),this.layout.min_height)):(null!==this.layout.proportional_width&&(this.layout.width=Math.max(this.layout.proportional_width*this.parent.layout.width,this.layout.min_width)),null!==this.layout.proportional_height&&(this.layout.height=Math.max(this.layout.proportional_height*this.parent.layout.height,this.layout.min_height))),this.layout.cliparea.width=Math.max(this.layout.width-(this.layout.margin.left+this.layout.margin.right),0),this.layout.cliparea.height=Math.max(this.layout.height-(this.layout.margin.top+this.layout.margin.bottom),0),this.svg.clipRect&&this.svg.clipRect.attr("width",this.layout.width).attr("height",this.layout.height),this.initialized&&(this.render(),this.curtain.update(),this.loader.update(),this.dashboard.update(),this.legend&&this.legend.position()),this},n.Panel.prototype.setOrigin=function(t,e){return!isNaN(t)&&t>=0&&(this.layout.origin.x=Math.max(Math.round(+t),0)),!isNaN(e)&&e>=0&&(this.layout.origin.y=Math.max(Math.round(+e),0)),this.initialized&&this.render(),this},n.Panel.prototype.setMargin=function(t,e,a,i){var n;return!isNaN(t)&&t>=0&&(this.layout.margin.top=Math.max(Math.round(+t),0)),!isNaN(e)&&e>=0&&(this.layout.margin.right=Math.max(Math.round(+e),0)),!isNaN(a)&&a>=0&&(this.layout.margin.bottom=Math.max(Math.round(+a),0)),!isNaN(i)&&i>=0&&(this.layout.margin.left=Math.max(Math.round(+i),0)),this.layout.margin.top+this.layout.margin.bottom>this.layout.height&&(n=Math.floor((this.layout.margin.top+this.layout.margin.bottom-this.layout.height)/2),this.layout.margin.top-=n, +this.layout.margin.bottom-=n),this.layout.margin.left+this.layout.margin.right>this.layout.width&&(n=Math.floor((this.layout.margin.left+this.layout.margin.right-this.layout.width)/2),this.layout.margin.left-=n,this.layout.margin.right-=n),["top","right","bottom","left"].forEach(function(t){this.layout.margin[t]=Math.max(this.layout.margin[t],0)}.bind(this)),this.layout.cliparea.width=Math.max(this.layout.width-(this.layout.margin.left+this.layout.margin.right),0),this.layout.cliparea.height=Math.max(this.layout.height-(this.layout.margin.top+this.layout.margin.bottom),0),this.layout.cliparea.origin.x=this.layout.margin.left,this.layout.cliparea.origin.y=this.layout.margin.top,this.initialized&&this.render(),this},n.Panel.prototype.setTitle=function(t){if("string"==typeof this.layout.title){var e=this.layout.title;this.layout.title={text:e,x:0,y:0,style:{}}}return"string"==typeof t?this.layout.title.text=t:"object"==typeof t&&null!==t&&(this.layout.title=n.Layouts.merge(t,this.layout.title)),this.layout.title.text.length?this.title.attr("display",null).attr("x",parseFloat(this.layout.title.x)).attr("y",parseFloat(this.layout.title.y)).style(this.layout.title.style).text(this.layout.title.text):this.title.attr("display","none"),this},n.Panel.prototype.initialize=function(){this.svg.container=this.parent.svg.append("g").attr("id",this.getBaseId()+".panel_container").attr("transform","translate("+(this.layout.origin.x||0)+","+(this.layout.origin.y||0)+")");var t=this.svg.container.append("clipPath").attr("id",this.getBaseId()+".clip");if(this.svg.clipRect=t.append("rect").attr("width",this.layout.width).attr("height",this.layout.height),this.svg.group=this.svg.container.append("g").attr("id",this.getBaseId()+".panel").attr("clip-path","url(#"+this.getBaseId()+".clip)"),this.curtain=n.generateCurtain.call(this),this.loader=n.generateLoader.call(this),this.dashboard=new n.Dashboard(this),this.inner_border=this.svg.group.append("rect").attr("class","lz-panel-background").on("click",function(){"clear_selections"===this.layout.background_click&&this.clearSelections()}.bind(this)),this.title=this.svg.group.append("text").attr("class","lz-panel-title"),"undefined"!=typeof this.layout.title&&this.setTitle(),this.svg.x_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".x_axis").attr("class","lz-x lz-axis"),this.layout.axes.x.render&&(this.svg.x_axis_label=this.svg.x_axis.append("text").attr("class","lz-x lz-axis lz-label").attr("text-anchor","middle")),this.svg.y1_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".y1_axis").attr("class","lz-y lz-y1 lz-axis"),this.layout.axes.y1.render&&(this.svg.y1_axis_label=this.svg.y1_axis.append("text").attr("class","lz-y1 lz-axis lz-label").attr("text-anchor","middle")),this.svg.y2_axis=this.svg.group.append("g").attr("id",this.getBaseId()+".y2_axis").attr("class","lz-y lz-y2 lz-axis"),this.layout.axes.y2.render&&(this.svg.y2_axis_label=this.svg.y2_axis.append("text").attr("class","lz-y2 lz-axis lz-label").attr("text-anchor","middle")),this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].initialize()}.bind(this)),this.legend=null,this.layout.legend&&(this.legend=new n.Legend(this)),this.layout.interaction.drag_background_to_pan){var e="."+this.parent.id+"."+this.id+".interaction.drag",a=function(){this.parent.startDrag(this,"background")}.bind(this);this.svg.container.select(".lz-panel-background").on("mousedown"+e+".background",a).on("touchstart"+e+".background",a)}return this},n.Panel.prototype.resortDataLayers=function(){var e=[];this.data_layer_ids_by_z_index.forEach(function(t){e.push(this.data_layers[t].layout.z_index)}.bind(this)),this.svg.group.selectAll("g.lz-data_layer-container").data(e).sort(t.ascending),this.applyDataLayerZIndexesToDataLayerLayouts()},n.Panel.prototype.getLinkedPanelIds=function(t){t=t||null;var e=[];return["x","y1","y2"].indexOf(t)===-1?e:this.layout.interaction[t+"_linked"]?(this.parent.panel_ids_by_y_index.forEach(function(a){a!==this.id&&this.parent.panels[a].layout.interaction[t+"_linked"]&&e.push(a)}.bind(this)),e):e},n.Panel.prototype.moveUp=function(){return this.parent.panel_ids_by_y_index[this.layout.y_index-1]&&(this.parent.panel_ids_by_y_index[this.layout.y_index]=this.parent.panel_ids_by_y_index[this.layout.y_index-1],this.parent.panel_ids_by_y_index[this.layout.y_index-1]=this.id,this.parent.applyPanelYIndexesToPanelLayouts(),this.parent.positionPanels()),this},n.Panel.prototype.moveDown=function(){return this.parent.panel_ids_by_y_index[this.layout.y_index+1]&&(this.parent.panel_ids_by_y_index[this.layout.y_index]=this.parent.panel_ids_by_y_index[this.layout.y_index+1],this.parent.panel_ids_by_y_index[this.layout.y_index+1]=this.id,this.parent.applyPanelYIndexesToPanelLayouts(),this.parent.positionPanels()),this},n.Panel.prototype.addDataLayer=function(t){if("object"!=typeof t||"string"!=typeof t.id||!t.id.length)throw"Invalid data layer layout passed to LocusZoom.Panel.prototype.addDataLayer()";if("undefined"!=typeof this.data_layers[t.id])throw"Cannot create data_layer with id ["+t.id+"]; data layer with that id already exists in the panel";if("string"!=typeof t.type)throw"Invalid data layer type in layout passed to LocusZoom.Panel.prototype.addDataLayer()";"object"!=typeof t.y_axis||"undefined"!=typeof t.y_axis.axis&&[1,2].indexOf(t.y_axis.axis)!==-1||(t.y_axis.axis=1);var e=n.DataLayers.get(t.type,t,this);if(this.data_layers[e.id]=e,null!==e.layout.z_index&&!isNaN(e.layout.z_index)&&this.data_layer_ids_by_z_index.length>0)e.layout.z_index<0&&(e.layout.z_index=Math.max(this.data_layer_ids_by_z_index.length+e.layout.z_index,0)),this.data_layer_ids_by_z_index.splice(e.layout.z_index,0,e.id),this.data_layer_ids_by_z_index.forEach(function(t,e){this.data_layers[t].layout.z_index=e}.bind(this));else{var a=this.data_layer_ids_by_z_index.push(e.id);this.data_layers[e.id].layout.z_index=a-1}var i=null;return this.layout.data_layers.forEach(function(t,a){t.id===e.id&&(i=a)}),null===i&&(i=this.layout.data_layers.push(this.data_layers[e.id].layout)-1),this.data_layers[e.id].layout_idx=i,this.data_layers[e.id]},n.Panel.prototype.removeDataLayer=function(t){if(!this.data_layers[t])throw"Unable to remove data layer, ID not found: "+t;return this.data_layers[t].destroyAllTooltips(),this.data_layers[t].svg.container&&this.data_layers[t].svg.container.remove(),this.layout.data_layers.splice(this.data_layers[t].layout_idx,1),delete this.state[this.data_layers[t].state_id],delete this.data_layers[t],this.data_layer_ids_by_z_index.splice(this.data_layer_ids_by_z_index.indexOf(t),1),this.applyDataLayerZIndexesToDataLayerLayouts(),this.layout.data_layers.forEach(function(t,e){this.data_layers[t.id].layout_idx=e}.bind(this)),this},n.Panel.prototype.clearSelections=function(){return this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].setAllElementStatus("selected",!1)}.bind(this)),this},n.Panel.prototype.reMap=function(){this.emit("data_requested"),this.data_promises=[],this.curtain.hide();for(var t in this.data_layers)try{this.data_promises.push(this.data_layers[t].reMap())}catch(t){console.warn(t),this.curtain.show(t)}return e.all(this.data_promises).then(function(){this.initialized=!0,this.render(),this.emit("layout_changed"),this.parent.emit("layout_changed"),this.emit("data_rendered")}.bind(this)).catch(function(t){console.warn(t),this.curtain.show(t)}.bind(this))};n.Panel.prototype.generateExtents=function(){["x","y1","y2"].forEach(function(t){this[t+"_extent"]=null}.bind(this));for(var e in this.data_layers){var a=this.data_layers[e];if(a.layout.x_axis&&!a.layout.x_axis.decoupled&&(this.x_extent=t.extent((this.x_extent||[]).concat(a.getAxisExtent("x")))),a.layout.y_axis&&!a.layout.y_axis.decoupled){var i="y"+a.layout.y_axis.axis;this[i+"_extent"]=t.extent((this[i+"_extent"]||[]).concat(a.getAxisExtent("y")))}}return this.layout.axes.x&&"state"===this.layout.axes.x.extent&&(this.x_extent=[this.state.start,this.state.end]),this};n.Panel.prototype.generateTicks=function(t){if(this.layout.axes[t].ticks){var e=this.layout.axes[t],a=e.ticks;if(Array.isArray(a))return a;if("object"==typeof a){var i=this,s={position:a.position},o=this.data_layer_ids_by_z_index.reduce(function(e,a){var n=i.data_layers[a];return e.concat(n.getTicks(t,s))},[]);return o.map(function(t){var e={};return e=n.Layouts.merge(e,a),n.Layouts.merge(e,t)})}}return this[t+"_extent"]?n.prettyTicks(this[t+"_extent"],"both"):[]},n.Panel.prototype.render=function(){this.svg.container.attr("transform","translate("+this.layout.origin.x+","+this.layout.origin.y+")"),this.svg.clipRect.attr("width",this.layout.width).attr("height",this.layout.height),this.inner_border.attr("x",this.layout.margin.left).attr("y",this.layout.margin.top).attr("width",this.layout.width-(this.layout.margin.left+this.layout.margin.right)).attr("height",this.layout.height-(this.layout.margin.top+this.layout.margin.bottom)),this.layout.inner_border&&this.inner_border.style({"stroke-width":1,stroke:this.layout.inner_border}),this.setTitle(),this.generateExtents();var e=function(t,e){var a=Math.pow(-10,e),i=Math.pow(-10,-e),n=Math.pow(10,-e),s=Math.pow(10,e);return t===1/0&&(t=s),t===-(1/0)&&(t=a),0===t&&(t=n),t>0&&(t=Math.max(Math.min(t,s),n)),t<0&&(t=Math.max(Math.min(t,i),a)),t},a={};if(this.x_extent){var i={start:0,end:this.layout.cliparea.width};this.layout.axes.x.range&&(i.start=this.layout.axes.x.range.start||i.start,i.end=this.layout.axes.x.range.end||i.end),a.x=[i.start,i.end],a.x_shifted=[i.start,i.end]}if(this.y1_extent){var n={start:this.layout.cliparea.height,end:0};this.layout.axes.y1.range&&(n.start=this.layout.axes.y1.range.start||n.start,n.end=this.layout.axes.y1.range.end||n.end),a.y1=[n.start,n.end],a.y1_shifted=[n.start,n.end]}if(this.y2_extent){var s={start:this.layout.cliparea.height,end:0};this.layout.axes.y2.range&&(s.start=this.layout.axes.y2.range.start||s.start,s.end=this.layout.axes.y2.range.end||s.end),a.y2=[s.start,s.end],a.y2_shifted=[s.start,s.end]}if(this.parent.interaction.panel_id&&(this.parent.interaction.panel_id===this.id||this.parent.interaction.linked_panel_ids.indexOf(this.id)!==-1)){var o,r=null;if(this.parent.interaction.zooming&&"function"==typeof this.x_scale){var l=Math.abs(this.x_extent[1]-this.x_extent[0]),h=Math.round(this.x_scale.invert(a.x_shifted[1]))-Math.round(this.x_scale.invert(a.x_shifted[0])),d=this.parent.interaction.zooming.scale,u=Math.floor(h*(1/d));d<1&&!isNaN(this.parent.layout.max_region_scale)?d=1/(Math.min(u,this.parent.layout.max_region_scale)/h):d>1&&!isNaN(this.parent.layout.min_region_scale)&&(d=1/(Math.max(u,this.parent.layout.min_region_scale)/h));var p=Math.floor(l*d);o=this.parent.interaction.zooming.center-this.layout.margin.left-this.layout.origin.x;var c=o/this.layout.cliparea.width,y=Math.max(Math.floor(this.x_scale.invert(a.x_shifted[0])-(p-h)*c),1);a.x_shifted=[this.x_scale(y),this.x_scale(y+p)]}else if(this.parent.interaction.dragging)switch(this.parent.interaction.dragging.method){case"background":a.x_shifted[0]=+this.parent.interaction.dragging.dragged_x,a.x_shifted[1]=this.layout.cliparea.width+this.parent.interaction.dragging.dragged_x;break;case"x_tick":t.event&&t.event.shiftKey?(a.x_shifted[0]=+this.parent.interaction.dragging.dragged_x,a.x_shifted[1]=this.layout.cliparea.width+this.parent.interaction.dragging.dragged_x):(o=this.parent.interaction.dragging.start_x-this.layout.margin.left-this.layout.origin.x,r=e(o/(o+this.parent.interaction.dragging.dragged_x),3),a.x_shifted[0]=0,a.x_shifted[1]=Math.max(this.layout.cliparea.width*(1/r),1));break;case"y1_tick":case"y2_tick":var f="y"+this.parent.interaction.dragging.method[1]+"_shifted";t.event&&t.event.shiftKey?(a[f][0]=this.layout.cliparea.height+this.parent.interaction.dragging.dragged_y,a[f][1]=+this.parent.interaction.dragging.dragged_y):(o=this.layout.cliparea.height-(this.parent.interaction.dragging.start_y-this.layout.margin.top-this.layout.origin.y),r=e(o/(o-this.parent.interaction.dragging.dragged_y),3),a[f][0]=this.layout.cliparea.height,a[f][1]=this.layout.cliparea.height-this.layout.cliparea.height*(1/r))}}if(["x","y1","y2"].forEach(function(e){this[e+"_extent"]&&(this[e+"_scale"]=t.scale.linear().domain(this[e+"_extent"]).range(a[e+"_shifted"]),this[e+"_extent"]=[this[e+"_scale"].invert(a[e][0]),this[e+"_scale"].invert(a[e][1])],this[e+"_scale"]=t.scale.linear().domain(this[e+"_extent"]).range(a[e]),this.renderAxis(e))}.bind(this)),this.layout.interaction.scroll_to_zoom){var g=function(){if(!t.event.shiftKey)return void(this.parent.canInteract(this.id)&&this.loader.show("Press [SHIFT] while scrolling to zoom").hide(1e3));if(t.event.preventDefault(),this.parent.canInteract(this.id)){var e=t.mouse(this.svg.container.node()),a=Math.max(-1,Math.min(1,t.event.wheelDelta||-t.event.detail||-t.event.deltaY));0!==a&&(this.parent.interaction={panel_id:this.id,linked_panel_ids:this.getLinkedPanelIds("x"),zooming:{scale:a<1?.9:1.1,center:e[0]}},this.render(),this.parent.interaction.linked_panel_ids.forEach(function(t){this.parent.panels[t].render()}.bind(this)),null!==this.zoom_timeout&&clearTimeout(this.zoom_timeout),this.zoom_timeout=setTimeout(function(){this.parent.interaction={},this.parent.applyState({start:this.x_extent[0],end:this.x_extent[1]})}.bind(this),500))}}.bind(this);this.zoom_listener=t.behavior.zoom(),this.svg.container.call(this.zoom_listener).on("wheel.zoom",g).on("mousewheel.zoom",g).on("DOMMouseScroll.zoom",g)}return this.data_layer_ids_by_z_index.forEach(function(t){this.data_layers[t].draw().render()}.bind(this)),this},n.Panel.prototype.renderAxis=function(e){if(["x","y1","y2"].indexOf(e)===-1)throw"Unable to render axis; invalid axis identifier: "+e;var a=this.layout.axes[e].render&&"function"==typeof this[e+"_scale"]&&!isNaN(this[e+"_scale"](0));if(this[e+"_axis"]&&this.svg.container.select("g.lz-axis.lz-"+e).style("display",a?null:"none"),!a)return this;var i={x:{position:"translate("+this.layout.margin.left+","+(this.layout.height-this.layout.margin.bottom)+")",orientation:"bottom",label_x:this.layout.cliparea.width/2,label_y:this.layout.axes[e].label_offset||0,label_rotate:null},y1:{position:"translate("+this.layout.margin.left+","+this.layout.margin.top+")",orientation:"left",label_x:-1*(this.layout.axes[e].label_offset||0),label_y:this.layout.cliparea.height/2,label_rotate:-90},y2:{position:"translate("+(this.layout.width-this.layout.margin.right)+","+this.layout.margin.top+")",orientation:"right",label_x:this.layout.axes[e].label_offset||0,label_y:this.layout.cliparea.height/2,label_rotate:-90}};this[e+"_ticks"]=this.generateTicks(e);var s=function(t){for(var e=0;e",">=","%","matches","filterIndexes","filterElements","verb","adjective","antiverb","setElementStatus","setElementStatusByFilters","setAllElementStatus","toggle","get_element_id_error","element_status_node_id","element_status_idx","splice","emit","status_ids","applyBehaviors","selection","event_match","executeBehaviors","requiredKeyStates","ctrl","ctrlKey","shiftKey","behavior","current_status_boolean","href","target","window","location","panel_origin","exportData","format","default_format","toLowerCase","e","jsonified","delimiter","record","draw","cliparea","reMap","lzd","getData","new_data","DataLayers","datalayers","datalayer","extend","parent_name","overrides","child","render","self","trackData","enter","exit","arrow_type","arrow_top","arrow_left","arrow_width","stroke_width","tooltip_box","data_layer_height","data_layer_width","x_center","x_scale","y_center","offset_right","offset_left","confidence_intervals","show_no_significance_line","border_radius","y_scale","sqrt","PI","ci_selection","ci_transform","ci_width","ci_height","duration","ease","points_selection","initial_y","symbol","label_font_size","label_exon_spacing","exon_height","bounding_box_padding","track_vertical_spacing","getTrackHeight","transcript_idx","tracks","gene_track_index","1","assignTracks","getLabelWidth","gene_name","font_size","temp_text","label_width","getBBox","g","gene_id","split","gene_version","transcript_id","transcripts","display_range","text_anchor","centered_margin","display_domain","invert","track","potential_track","collision_on_potential_track","placed_gene","min_start","max_end","t","exons","bboxes","boundaries","labels","strand","exon_id","clickareas","gene_bbox_id","gene_bbox","gene_center_x","chromosome_fill_colors","light","dark","chromosome_label_colors","genome_start","genome_end","chromosomes","variant_parts","variant","track_split_order","track_split_legend_to_y_axis","track_height","previous_tracks","interval_track_index","track_split_field_index","reverse","placed_interval","psuedoElement","sharedstatusnode_style","display","interval","statusnode_style","statusnodes","rects","interval_name","updateSplitTrackAxis","interval_bbox","interval_center_x","legend_axis","track_spacing","target_height","scaleHeightToData","toggleSplitTracks","interpolate","hitarea_width","mouse_event","line","tooltip_timeout","getMouseDisplayAndData","mouse","slope","x_field","y_field","bisect","bisector","datum","startDatum","endDatum","interpolateNumber","x_precision","toPrecision","y_precision","dd","min_arrow_left","max_arrow_left","path","hitarea","hitarea_line","path_class","global_status","decoupled","x_extent","y_extent","x_range","y_range","flip_labels","handle_lines","Boolean","min_x","max_x","flip","dn","dnl","dnx","text_swing","dnlx2","line_swing","label_texts","da","dax","abound","dal","label_lines","db","bbound","collision","separate_labels","seperate_iterations","alpha","again","delta","sign","adjust","new_a_y","new_b_y","min_y","max_y","label_elements","label_line","filtered_data","label_groups","x1","x2","makeLDReference","ref","applyState","ldrefvar","_prepareData","xField","sourceData","sort","ak","bk","av","bv","_generateCategoryBounds","uniqueCategories","item","category","bounds","categoryNames","_setDynamicColorScheme","colorParams","baseParams","parameters_categories_hash","every","colors","color_scale","scale","category10","category20","concat","categoryBounds","_categories","knownCategories","knownColors","xPos","diff","KnownDataSources","sources","findSourceByName","SOURCE_NAME","source","warn","source_name","newObj","params","Function","getAll","setAll","clear","TransformationFunctions","getTrans","fun","parseTrans","parseTransString","result","funs","substring","fn","ceil","toExponential","str","encodeURIComponent","s","functions","input","threshold","prev","curr","nullval","upper_idx","brk","normalized_input","isFinite","Dashboard","hide_timeout","persist","component","Components","shouldPersist","visibility","destroy","force","Component","parent_panel","parent_svg","button","menu","Button","parent_dashboard","tag","setTag","setHtml","setText","setHTML","setTitle","setColor","setStyle","getClass","permanent","setPermanent","bool","setStatus","highlight","disable","setOnMouseover","setOnMouseout","setOnclick","preUpdate","postUpdate","outer_selector","inner_selector","scroll_position","scrollTop","scrollbar_padding","menu_height_padding","page_scroll_top","document","documentElement","container_offset","getContainerOffset","dashboard_client_rect","button_client_rect","menu_client_rect","total_content_height","scrollHeight","base_max_width","container_max_width","content_max_width","base_max_height","max_height","setPopulate","menu_populate_function","div_selector","title_selector","display_width","display_height","generateBase64SVG","base64_string","css_string","stylesheet","styleSheets","fcall","outerHTML","dy","initial_html","style_def","insert_at","btoa","p1","String","fromCharCode","suppress_confirm","confirm","removePanel","is_at_top","y_index","is_at_bottom","panel_ids_by_y_index","can_zoom","current_region_scale","zoom_factor","new_region_scale","menu_html","model","covariates","CovariatesModel","element_reference","updateComponent","removeByIdx","removeAll","table","covariate","row","cov","scale_timeout","status_adj","status_idx","status_verb","at_top","at_bottom","td","removeDataLayer","allowed_fields","fields_whitelist","dataLayer","layer_name","dataLayerLayout","defaultConfig","configSlot","_selected_item","uniqueID","random","menuLayout","renderRow","display_name","display_options","row_id","field_name","defaultName","default_config_display_name","options","Legend","background_rect","elements","elements_group","label_size","line_height","label_x","label_y","path_y","symbolTypes","radius","bcr","right_x","pad_from_right","DataSources","addSource","ns","dsobj","getSource","removeSource","fromJSON","ds","toJSON","parts","full_name","applyTransformations","Requester","split_requests","requests","raw","trans","outnames","promises","when","Source","enableCache","dependentSource","parseInit","init","getCacheKey","chain","getURL","fetchRequest","getRequest","req","cacheKey","_cachedKey","_cachedResponse","preGetData","pre","resp","parseResponse","json","records","parseData","parseArraysToObjects","N","sameLength","j","parseObjectsToObjects","fieldFound","v","prepareData","constructorFun","uniqueName","getPrototypeOf","AssociationSource","unshift","analysis","LDSource","findMergeFields","exactMatch","arr","regexes","dataFields","position_field","pvalue","pvalue_field","_names_","names","nameMatch","findRequestedFields","isrefvarin","isrefvarout","ldin","ldout","findExtremeValue","pval","extremeVal","extremeIdx","refSource","ldrefsource","reqFields","refVar","columns","leftJoin","lfield","rfield","position2","tagRefVariant","refvar","idfield","outname","GeneSource","GeneConstraintSource","geneids","substr","Content-Type","constraint_fields","RecombinationRateSource","recombsource","IntervalSource","bedtracksource","StaticSource","_data","PheWASSource","build","applyPanelYIndexesToPanelLayouts","pid","remap_promises","window_onresize","event_hooks","layout_changed","data_requested","data_rendered","element_clicked","hook","context","hookToRun","bounding_client_rect","x_offset","scrollLeft","y_offset","offsetParent","offsetTop","offsetLeft","canInteract","loading_data","zooming","initializeLayout","aspect_ratio","sumProportional","total","rescaleSVG","clientRect","panel_layout","addPanel","min-width","min-height","panel_width","panel_height","setOrigin","proportional_origin","clearPanelData","panelId","mode","panelsList","dlid","layer","x_linked_margins","total_proportional_height","proportional_adjustment","calculated_plot_height","mouse_guide_svg","mouse_guide_vertical_svg","mouse_guide_horizontal_svg","vertical","horizontal","selectors","corner_selector","panel_idx","panel_resize_drag","drag","this_panel","original_panel_height","panel_height_change","new_calculated_plot_height","loop_panel_id","loop_panel_idx","loop_panel","corner_drag","dx","plot_page_origin","panel_page_origin","corner_padding","corner_size","mouseout_mouse_guide","mousemove_mouse_guide","coords","mouseup","stopDrag","mousemove","preventDefault","dragged_x","start_x","dragged_y","start_y","linked_panel_ids","client_rect","state_changes","all","catch","drop","startDrag","getLinkedPanelIds","overrideAxisLayout","axis_number","y_axis_number","generateID","applyDataLayerZIndexesToDataLayerLayouts","data_promises","y1_scale","y2_scale","y1_extent","y2_extent","x_ticks","y1_ticks","y2_ticks","zoom_timeout","plot_origin","background_click","y1_linked","y2_linked","panel_count","setMargin","y1_range","y2_range","label_function","data_layer_layout","addDataLayer","clipPath","clearSelections","x_axis_label","y1_axis","y1_axis_label","y2_axis","y2_axis_label","mousedown","ascending","generateExtents","generateTicks","baseTickConfig","combinedTicks","acc","nextLayer","itemConfig","constrain","limit_exponent","neg_min","neg_max","pos_min","pos_max","Infinity","ranges","base_x_range","x_shifted","base_y1_range","y1_shifted","base_y2_range","y2_shifted","anchor","scalar","current_extent_size","current_scaled_extent_size","potential_extent_size","new_extent_size","offset_ratio","new_x_extent_start","y_shifted","linear","domain","renderAxis","zoom_handler","wheelDelta","detail","deltaY","zoom_listener","zoom","canRender","axis_params","label_rotate","ticksAreAllNumbers","orient","tickPadding","tickValues","tickFormat","tick_selector","tick_mouseover","focus","cursor","dh","addBasicLoader","show_immediately"],"mappings":"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","file":"locuszoom.app.min.js","sourcesContent":["/**\n * @namespace\n */\nvar LocusZoom = {\n version: \"0.7.2\"\n};\n\n/**\n * Populate a single element with a LocusZoom plot.\n * selector can be a string for a DOM Query or a d3 selector.\n * @param {String} selector CSS selector for the container element where the plot will be mounted. Any pre-existing\n * content in the container will be completely replaced.\n * @param {LocusZoom.DataSources} datasource Ensemble of data providers used by the plot\n * @param {Object} layout A JSON-serializable object of layout configuration parameters\n * @returns {LocusZoom.Plot} The newly created plot instance\n */\nLocusZoom.populate = function(selector, datasource, layout) {\n if (typeof selector == \"undefined\"){\n throw (\"LocusZoom.populate selector not defined\");\n }\n // Empty the selector of any existing content\n d3.select(selector).html(\"\");\n var plot;\n d3.select(selector).call(function(){\n // Require each containing element have an ID. If one isn't present, create one.\n if (typeof this.node().id == \"undefined\"){\n var iterator = 0;\n while (!d3.select(\"#lz-\" + iterator).empty()){ iterator++; }\n this.attr(\"id\", \"#lz-\" + iterator);\n }\n // Create the plot\n plot = new LocusZoom.Plot(this.node().id, datasource, layout);\n plot.container = this.node();\n // Detect data-region and fill in state values if present\n if (typeof this.node().dataset !== \"undefined\" && typeof this.node().dataset.region !== \"undefined\"){\n var parsed_state = LocusZoom.parsePositionQuery(this.node().dataset.region);\n Object.keys(parsed_state).forEach(function(key){\n plot.state[key] = parsed_state[key];\n });\n }\n // Add an SVG to the div and set its dimensions\n plot.svg = d3.select(\"div#\" + plot.id)\n .append(\"svg\")\n .attr(\"version\", \"1.1\")\n .attr(\"xmlns\", \"http://www.w3.org/2000/svg\")\n .attr(\"id\", plot.id + \"_svg\").attr(\"class\", \"lz-locuszoom\")\n .style(plot.layout.style);\n plot.setDimensions();\n plot.positionPanels();\n // Initialize the plot\n plot.initialize();\n // If the plot has defined data sources then trigger its first mapping based on state values\n if (typeof datasource == \"object\" && Object.keys(datasource).length){\n plot.refresh();\n }\n });\n return plot;\n};\n\n/**\n * Populate arbitrarily many elements each with a LocusZoom plot\n * using a common datasource and layout\n * @param {String} selector CSS selector for the container element where the plot will be mounted. Any pre-existing\n * content in the container will be completely replaced.\n * @param {LocusZoom.DataSources} datasource Ensemble of data providers used by the plot\n * @param {Object} layout A JSON-serializable object of layout configuration parameters\n * @returns {LocusZoom.Plot[]}\n */\nLocusZoom.populateAll = function(selector, datasource, layout) {\n var plots = [];\n d3.selectAll(selector).each(function(d,i) {\n plots[i] = LocusZoom.populate(this, datasource, layout);\n });\n return plots;\n};\n\n/**\n * Convert an integer chromosome position to an SI string representation (e.g. 23423456 => \"23.42\" (Mb))\n * @param {Number} pos Position\n * @param {String} [exp] Exponent to use for the returned string, eg 6=> MB. If not specified, will attempt to guess\n * the most appropriate SI prefix based on the number provided.\n * @param {Boolean} [suffix=false] Whether or not to append a suffix (e.g. \"Mb\") to the end of the returned string\n * @returns {string}\n */\nLocusZoom.positionIntToString = function(pos, exp, suffix){\n var exp_symbols = { 0: \"\", 3: \"K\", 6: \"M\", 9: \"G\" };\n suffix = suffix || false;\n if (isNaN(exp) || exp === null){\n var log = Math.log(pos) / Math.LN10;\n exp = Math.min(Math.max(log - (log % 3), 0), 9);\n }\n var places_exp = exp - Math.floor((Math.log(pos) / Math.LN10).toFixed(exp + 3));\n var min_exp = Math.min(Math.max(exp, 0), 2);\n var places = Math.min(Math.max(places_exp, min_exp), 12);\n var ret = \"\" + (pos / Math.pow(10, exp)).toFixed(places);\n if (suffix && typeof exp_symbols[exp] !== \"undefined\"){\n ret += \" \" + exp_symbols[exp] + \"b\";\n }\n return ret;\n};\n\n/**\n * Convert an SI string chromosome position to an integer representation (e.g. \"5.8 Mb\" => 58000000)\n * @param {String} p The chromosome position\n * @returns {Number}\n */\nLocusZoom.positionStringToInt = function(p) {\n var val = p.toUpperCase();\n val = val.replace(/,/g, \"\");\n var suffixre = /([KMG])[B]*$/;\n var suffix = suffixre.exec(val);\n var mult = 1;\n if (suffix) {\n if (suffix[1]===\"M\") {\n mult = 1e6;\n } else if (suffix[1]===\"G\") {\n mult = 1e9;\n } else {\n mult = 1e3; //K\n }\n val = val.replace(suffixre,\"\");\n }\n val = Number(val) * mult;\n return val;\n};\n\n/**\n * Parse region queries into their constituent parts\n * TODO: handle genes (or send off to API)\n * @param {String} x A chromosome position query. May be any of the forms `chr:start-end`, `chr:center+offset`,\n * or `chr:pos`\n * @returns {{chr:*, start: *, end:*} | {chr:*, position:*}}\n */\nLocusZoom.parsePositionQuery = function(x) {\n var chrposoff = /^(\\w+):([\\d,.]+[kmgbKMGB]*)([-+])([\\d,.]+[kmgbKMGB]*)$/;\n var chrpos = /^(\\w+):([\\d,.]+[kmgbKMGB]*)$/;\n var match = chrposoff.exec(x);\n if (match) {\n if (match[3] === \"+\") {\n var center = LocusZoom.positionStringToInt(match[2]);\n var offset = LocusZoom.positionStringToInt(match[4]);\n return {\n chr:match[1],\n start: center - offset,\n end: center + offset\n };\n } else {\n return {\n chr: match[1],\n start: LocusZoom.positionStringToInt(match[2]),\n end: LocusZoom.positionStringToInt(match[4])\n };\n }\n }\n match = chrpos.exec(x);\n if (match) {\n return {\n chr:match[1],\n position: LocusZoom.positionStringToInt(match[2])\n };\n }\n return null;\n};\n\n/**\n * Generate a \"pretty\" set of ticks (multiples of 1, 2, or 5 on the same order of magnitude for the range)\n * Based on R's \"pretty\" function: https://github.com/wch/r-source/blob/b156e3a711967f58131e23c1b1dc1ea90e2f0c43/src/appl/pretty.c\n * @param {Number[]} range A two-item array specifying [low, high] values for the axis range\n * @param {('low'|'high'|'both'|'neither')} [clip_range='neither'] What to do if first and last generated ticks extend\n * beyond the range. Set this to \"low\", \"high\", \"both\", or \"neither\" to clip the first (low) or last (high) tick to\n * be inside the range or allow them to extend beyond.\n * e.g. \"low\" will clip the first (low) tick if it extends beyond the low end of the range but allow the\n * last (high) tick to extend beyond the range. \"both\" clips both ends, \"neither\" allows both to extend beyond.\n * @param {Number} [target_tick_count=5] The approximate number of ticks you would like to be returned; may not be exact\n * @returns {Number[]}\n */\nLocusZoom.prettyTicks = function(range, clip_range, target_tick_count){\n if (typeof target_tick_count == \"undefined\" || isNaN(parseInt(target_tick_count))){\n target_tick_count = 5;\n }\n target_tick_count = parseInt(target_tick_count);\n \n var min_n = target_tick_count / 3;\n var shrink_sml = 0.75;\n var high_u_bias = 1.5;\n var u5_bias = 0.5 + 1.5 * high_u_bias;\n \n var d = Math.abs(range[0] - range[1]);\n var c = d / target_tick_count;\n if ((Math.log(d) / Math.LN10) < -2){\n c = (Math.max(Math.abs(d)) * shrink_sml) / min_n;\n }\n \n var base = Math.pow(10, Math.floor(Math.log(c)/Math.LN10));\n var base_toFixed = 0;\n if (base < 1 && base !== 0){\n base_toFixed = Math.abs(Math.round(Math.log(base)/Math.LN10));\n }\n \n var unit = base;\n if ( ((2 * base) - c) < (high_u_bias * (c - unit)) ){\n unit = 2 * base;\n if ( ((5 * base) - c) < (u5_bias * (c - unit)) ){\n unit = 5 * base;\n if ( ((10 * base) - c) < (high_u_bias * (c - unit)) ){\n unit = 10 * base;\n }\n }\n }\n \n var ticks = [];\n var i = parseFloat( (Math.floor(range[0]/unit)*unit).toFixed(base_toFixed) );\n while (i < range[1]){\n ticks.push(i);\n i += unit;\n if (base_toFixed > 0){\n i = parseFloat(i.toFixed(base_toFixed));\n }\n }\n ticks.push(i);\n \n if (typeof clip_range == \"undefined\" || [\"low\", \"high\", \"both\", \"neither\"].indexOf(clip_range) === -1){\n clip_range = \"neither\";\n }\n if (clip_range === \"low\" || clip_range === \"both\"){\n if (ticks[0] < range[0]){ ticks = ticks.slice(1); }\n }\n if (clip_range === \"high\" || clip_range === \"both\"){\n if (ticks[ticks.length-1] > range[1]){ ticks.pop(); }\n }\n \n return ticks;\n};\n\n/**\n * Make an AJAX request and return a promise.\n * From http://www.html5rocks.com/en/tutorials/cors/\n * and with promises from https://gist.github.com/kriskowal/593076\n *\n * @param {String} method The HTTP verb\n * @param {String} url\n * @param {String} body The request body to send to the server\n * @param {Object} headers Object of custom request headers\n * @param {Number} [timeout] If provided, wait this long (in ms) before timing out\n * @returns {Promise}\n */\nLocusZoom.createCORSPromise = function (method, url, body, headers, timeout) {\n var response = Q.defer();\n var xhr = new XMLHttpRequest();\n if (\"withCredentials\" in xhr) {\n // Check if the XMLHttpRequest object has a \"withCredentials\" property.\n // \"withCredentials\" only exists on XMLHTTPRequest2 objects.\n xhr.open(method, url, true);\n } else if (typeof XDomainRequest != \"undefined\") {\n // Otherwise, check if XDomainRequest.\n // XDomainRequest only exists in IE, and is IE's way of making CORS requests.\n xhr = new XDomainRequest();\n xhr.open(method, url);\n } else {\n // Otherwise, CORS is not supported by the browser.\n xhr = null;\n }\n if (xhr) {\n xhr.onreadystatechange = function() {\n if (xhr.readyState === 4) {\n if (xhr.status === 200 || xhr.status === 0 ) {\n response.resolve(xhr.response);\n } else {\n response.reject(\"HTTP \" + xhr.status + \" for \" + url);\n }\n }\n };\n timeout && setTimeout(response.reject, timeout);\n body = typeof body !== \"undefined\" ? body : \"\";\n if (typeof headers !== \"undefined\"){\n for (var header in headers){\n xhr.setRequestHeader(header, headers[header]);\n }\n }\n // Send the request\n xhr.send(body);\n } \n return response.promise;\n};\n\n/**\n * Validate a (presumed complete) plot state object against internal rules for consistency, and ensure the plot fits\n * within any constraints imposed by the layout.\n * @param {Object} new_state\n * @param {Number} new_state.start\n * @param {Number} new_state.end\n * @param {Object} layout\n * @returns {*|{}}\n */\nLocusZoom.validateState = function(new_state, layout){\n\n new_state = new_state || {};\n layout = layout || {};\n\n // If a \"chr\", \"start\", and \"end\" are present then resolve start and end\n // to numeric values that are not decimal, negative, or flipped\n var validated_region = false;\n if (typeof new_state.chr != \"undefined\" && typeof new_state.start != \"undefined\" && typeof new_state.end != \"undefined\"){\n // Determine a numeric scale and midpoint for the attempted region,\n var attempted_midpoint = null; var attempted_scale;\n new_state.start = Math.max(parseInt(new_state.start), 1);\n new_state.end = Math.max(parseInt(new_state.end), 1);\n if (isNaN(new_state.start) && isNaN(new_state.end)){\n new_state.start = 1;\n new_state.end = 1;\n attempted_midpoint = 0.5;\n attempted_scale = 0;\n } else if (isNaN(new_state.start) || isNaN(new_state.end)){\n attempted_midpoint = new_state.start || new_state.end;\n attempted_scale = 0;\n new_state.start = (isNaN(new_state.start) ? new_state.end : new_state.start);\n new_state.end = (isNaN(new_state.end) ? new_state.start : new_state.end);\n } else {\n attempted_midpoint = Math.round((new_state.start + new_state.end) / 2);\n attempted_scale = new_state.end - new_state.start;\n if (attempted_scale < 0){\n var temp = new_state.start;\n new_state.end = new_state.start;\n new_state.start = temp;\n attempted_scale = new_state.end - new_state.start;\n }\n if (attempted_midpoint < 0){\n new_state.start = 1;\n new_state.end = 1;\n attempted_scale = 0;\n }\n }\n validated_region = true;\n }\n\n // Constrain w/r/t layout-defined minimum region scale\n if (!isNaN(layout.min_region_scale) && validated_region && attempted_scale < layout.min_region_scale){\n new_state.start = Math.max(attempted_midpoint - Math.floor(layout.min_region_scale / 2), 1);\n new_state.end = new_state.start + layout.min_region_scale;\n }\n\n // Constrain w/r/t layout-defined maximum region scale\n if (!isNaN(layout.max_region_scale) && validated_region && attempted_scale > layout.max_region_scale){\n new_state.start = Math.max(attempted_midpoint - Math.floor(layout.max_region_scale / 2), 1);\n new_state.end = new_state.start + layout.max_region_scale;\n }\n\n return new_state;\n};\n\n//\n/**\n * Replace placeholders in an html string with field values defined in a data object\n * Only works on scalar values! Will ignore non-scalars.\n *\n * NOTE: Trusts content exactly as given. XSS prevention is the responsibility of the implementer.\n * @param {Object} data\n * @param {String} html A placeholder string in which to substitute fields. Supports several template options:\n * `{{field_name}}` is a variable placeholder for the value of `field_name` from the provided data\n * `{{#if {{field_name}} }} Conditional text {{/if}} will insert the contents of the tag only if the value exists.\n * Since this is only an existence check, **variables with a value of 0 will be evaluated as true**.\n * @returns {string}\n */\nLocusZoom.parseFields = function (data, html) {\n if (typeof data != \"object\"){\n throw (\"LocusZoom.parseFields invalid arguments: data is not an object\");\n }\n if (typeof html != \"string\"){\n throw (\"LocusZoom.parseFields invalid arguments: html is not a string\");\n }\n // `tokens` is like [token,...]\n // `token` is like {text: '...'} or {variable: 'foo|bar'} or {condition: 'foo|bar'} or {close: 'if'}\n var tokens = [];\n var regex = /\\{\\{(?:(#if )?([A-Za-z0-9_:|]+)|(\\/if))\\}\\}/;\n while (html.length > 0){\n var m = regex.exec(html);\n if (!m) { tokens.push({text: html}); html = \"\"; }\n else if (m.index !== 0) { tokens.push({text: html.slice(0, m.index)}); html = html.slice(m.index); }\n else if (m[1] === \"#if \") { tokens.push({condition: m[2]}); html = html.slice(m[0].length); }\n else if (m[2]) { tokens.push({variable: m[2]}); html = html.slice(m[0].length); }\n else if (m[3] === \"/if\") { tokens.push({close: \"if\"}); html = html.slice(m[0].length); }\n else {\n console.error(\"Error tokenizing tooltip when remaining template is \" + JSON.stringify(html) +\n \" and previous tokens are \" + JSON.stringify(tokens) +\n \" and current regex match is \" + JSON.stringify([m[1], m[2], m[3]]));\n html=html.slice(m[0].length);\n }\n }\n var astify = function() {\n var token = tokens.shift();\n if (typeof token.text !== \"undefined\" || token.variable) {\n return token;\n } else if (token.condition) {\n token.then = [];\n while(tokens.length > 0) {\n if (tokens[0].close === \"if\") { tokens.shift(); break; }\n token.then.push(astify());\n }\n return token;\n } else {\n console.error(\"Error making tooltip AST due to unknown token \" + JSON.stringify(token));\n return { text: \"\" };\n }\n };\n // `ast` is like [thing,...]\n // `thing` is like {text: \"...\"} or {variable:\"foo|bar\"} or {condition: \"foo|bar\", then:[thing,...]}\n var ast = [];\n while (tokens.length > 0) ast.push(astify());\n\n var resolve = function(variable) {\n if (!resolve.cache.hasOwnProperty(variable)) {\n resolve.cache[variable] = (new LocusZoom.Data.Field(variable)).resolve(data);\n }\n return resolve.cache[variable];\n };\n resolve.cache = {};\n var render_node = function(node) {\n if (typeof node.text !== \"undefined\") {\n return node.text;\n } else if (node.variable) {\n try {\n var value = resolve(node.variable);\n if ([\"string\",\"number\",\"boolean\"].indexOf(typeof value) !== -1) { return value; }\n if (value === null) { return \"\"; }\n } catch (error) { console.error(\"Error while processing variable \" + JSON.stringify(node.variable)); }\n return \"{{\" + node.variable + \"}}\";\n } else if (node.condition) {\n try {\n var condition = resolve(node.condition);\n if (condition || condition === 0) {\n return node.then.map(render_node).join(\"\");\n }\n } catch (error) { console.error(\"Error while processing condition \" + JSON.stringify(node.variable)); }\n return \"\";\n } else { console.error(\"Error rendering tooltip due to unknown AST node \" + JSON.stringify(node)); }\n };\n return ast.map(render_node).join(\"\");\n};\n\n/**\n * Shortcut method for getting the data bound to a tool tip.\n * @param {Element} node\n * @returns {*} The first element of data bound to the tooltip\n */\nLocusZoom.getToolTipData = function(node){\n if (typeof node != \"object\" || typeof node.parentNode == \"undefined\"){\n throw(\"Invalid node object\");\n }\n // If this node is a locuszoom tool tip then return its data\n var selector = d3.select(node);\n if (selector.classed(\"lz-data_layer-tooltip\") && typeof selector.data()[0] != \"undefined\"){\n return selector.data()[0];\n } else {\n return LocusZoom.getToolTipData(node.parentNode);\n }\n};\n\n/**\n * Shortcut method for getting a reference to the data layer that generated a tool tip.\n * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.getToolTipDataLayer = function(node){\n var data = LocusZoom.getToolTipData(node);\n if (data.getDataLayer){ return data.getDataLayer(); }\n return null;\n};\n\n/**\n * Shortcut method for getting a reference to the panel that generated a tool tip.\n * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.getToolTipPanel = function(node){\n var data_layer = LocusZoom.getToolTipDataLayer(node);\n if (data_layer){ return data_layer.parent; }\n return null;\n};\n\n/**\n * Shortcut method for getting a reference to the plot that generated a tool tip.\n * @param {Element} node The element associated with the tooltip, or any element contained inside the tooltip\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.getToolTipPlot = function(node){\n var panel = LocusZoom.getToolTipPanel(node);\n if (panel){ return panel.parent; }\n return null;\n};\n\n/**\n * Generate a curtain object for a plot, panel, or any other subdivision of a layout\n * The panel curtain, like the plot curtain is an HTML overlay that obscures the entire panel. It can be styled\n * arbitrarily and display arbitrary messages. It is useful for reporting error messages visually to an end user\n * when the error renders the panel unusable.\n * TODO: Improve type doc here\n * @returns {object}\n */\nLocusZoom.generateCurtain = function(){\n var curtain = {\n showing: false,\n selector: null,\n content_selector: null,\n hide_delay: null,\n\n /**\n * Generate the curtain. Any content (string) argument passed will be displayed in the curtain as raw HTML.\n * CSS (object) can be passed which will apply styles to the curtain and its content.\n * @param {string} content Content to be displayed on the curtain (as raw HTML)\n * @param {object} css Apply the specified styles to the curtain and its contents\n */\n show: function(content, css){\n if (!this.curtain.showing){\n this.curtain.selector = d3.select(this.parent_plot.svg.node().parentNode).insert(\"div\")\n .attr(\"class\", \"lz-curtain\").attr(\"id\", this.id + \".curtain\");\n this.curtain.content_selector = this.curtain.selector.append(\"div\").attr(\"class\", \"lz-curtain-content\");\n this.curtain.selector.append(\"div\").attr(\"class\", \"lz-curtain-dismiss\").html(\"Dismiss\")\n .on(\"click\", function(){\n this.curtain.hide();\n }.bind(this));\n this.curtain.showing = true;\n }\n return this.curtain.update(content, css);\n }.bind(this),\n\n /**\n * Update the content and css of the curtain that's currently being shown. This method also adjusts the size\n * and positioning of the curtain to ensure it still covers the entire panel with no overlap.\n * @param {string} content Content to be displayed on the curtain (as raw HTML)\n * @param {object} css Apply the specified styles to the curtain and its contents\n */\n update: function(content, css){\n if (!this.curtain.showing){ return this.curtain; }\n clearTimeout(this.curtain.hide_delay);\n // Apply CSS if provided\n if (typeof css == \"object\"){\n this.curtain.selector.style(css);\n }\n // Update size and position\n var page_origin = this.getPageOrigin();\n this.curtain.selector.style({\n top: page_origin.y + \"px\",\n left: page_origin.x + \"px\",\n width: this.layout.width + \"px\",\n height: this.layout.height + \"px\"\n });\n this.curtain.content_selector.style({\n \"max-width\": (this.layout.width - 40) + \"px\",\n \"max-height\": (this.layout.height - 40) + \"px\"\n });\n // Apply content if provided\n if (typeof content == \"string\"){\n this.curtain.content_selector.html(content);\n }\n return this.curtain;\n }.bind(this),\n\n /**\n * Remove the curtain\n * @param {number} delay Time to wait (in ms)\n */\n hide: function(delay){\n if (!this.curtain.showing){ return this.curtain; }\n // If a delay was passed then defer to a timeout\n if (typeof delay == \"number\"){\n clearTimeout(this.curtain.hide_delay);\n this.curtain.hide_delay = setTimeout(this.curtain.hide, delay);\n return this.curtain;\n }\n // Remove curtain\n this.curtain.selector.remove();\n this.curtain.selector = null;\n this.curtain.content_selector = null;\n this.curtain.showing = false;\n return this.curtain;\n }.bind(this)\n };\n return curtain;\n};\n\n/**\n * Generate a loader object for a plot, panel, or any other subdivision of a layout\n *\n * The panel loader is a small HTML overlay that appears in the lower left corner of the panel. It cannot be styled\n * arbitrarily, but can show a custom message and show a minimalist loading bar that can be updated to specific\n * completion percentages or be animated.\n * TODO Improve type documentation\n * @returns {object}\n */\nLocusZoom.generateLoader = function(){\n var loader = {\n showing: false,\n selector: null,\n content_selector: null,\n progress_selector: null,\n cancel_selector: null,\n\n /**\n * Show a loading indicator\n * @param {string} [content='Loading...'] Loading message (displayed as raw HTML)\n */\n show: function(content){\n // Generate loader\n if (!this.loader.showing){\n this.loader.selector = d3.select(this.parent_plot.svg.node().parentNode).insert(\"div\")\n .attr(\"class\", \"lz-loader\").attr(\"id\", this.id + \".loader\");\n this.loader.content_selector = this.loader.selector.append(\"div\")\n .attr(\"class\", \"lz-loader-content\");\n this.loader.progress_selector = this.loader.selector\n .append(\"div\").attr(\"class\", \"lz-loader-progress-container\")\n .append(\"div\").attr(\"class\", \"lz-loader-progress\");\n /* TODO: figure out how to make this cancel button work\n this.loader.cancel_selector = this.loader.selector.append(\"div\")\n .attr(\"class\", \"lz-loader-cancel\").html(\"Cancel\")\n .on(\"click\", function(){\n this.loader.hide();\n }.bind(this));\n */\n this.loader.showing = true;\n if (typeof content == \"undefined\"){ content = \"Loading...\"; }\n }\n return this.loader.update(content);\n }.bind(this),\n\n /**\n * Update the currently displayed loader and ensure the new content is positioned correctly.\n * @param {string} content The text to display (as raw HTML). If not a string, will be ignored.\n * @param {number} [percent] A number from 1-100. If a value is specified, it will stop all animations\n * in progress.\n */\n update: function(content, percent){\n if (!this.loader.showing){ return this.loader; }\n clearTimeout(this.loader.hide_delay);\n // Apply content if provided\n if (typeof content == \"string\"){\n this.loader.content_selector.html(content);\n }\n // Update size and position\n var padding = 6; // is there a better place to store/define this?\n var page_origin = this.getPageOrigin();\n var loader_boundrect = this.loader.selector.node().getBoundingClientRect();\n this.loader.selector.style({\n top: (page_origin.y + this.layout.height - loader_boundrect.height - padding) + \"px\",\n left: (page_origin.x + padding) + \"px\"\n });\n /* Uncomment this code when a functional cancel button can be shown\n var cancel_boundrect = this.loader.cancel_selector.node().getBoundingClientRect();\n this.loader.content_selector.style({\n \"padding-right\": (cancel_boundrect.width + padding) + \"px\"\n });\n */\n // Apply percent if provided\n if (typeof percent == \"number\"){\n this.loader.progress_selector.style({\n width: (Math.min(Math.max(percent, 1), 100)) + \"%\"\n });\n }\n return this.loader;\n }.bind(this),\n\n /**\n * Adds a class to the loading bar that makes it loop infinitely in a loading animation. Useful when exact\n * percent progress is not available.\n */\n animate: function(){\n this.loader.progress_selector.classed(\"lz-loader-progress-animated\", true);\n return this.loader;\n }.bind(this),\n\n /**\n * Sets the loading bar in the loader to percentage width equal to the percent (number) value passed. Percents\n * will automatically be limited to a range of 1 to 100. Will stop all animations in progress.\n */\n setPercentCompleted: function(percent){\n this.loader.progress_selector.classed(\"lz-loader-progress-animated\", false);\n return this.loader.update(null, percent);\n }.bind(this),\n\n /**\n * Remove the loader\n * @param {number} delay Time to wait (in ms)\n */\n hide: function(delay){\n if (!this.loader.showing){ return this.loader; }\n // If a delay was passed then defer to a timeout\n if (typeof delay == \"number\"){\n clearTimeout(this.loader.hide_delay);\n this.loader.hide_delay = setTimeout(this.loader.hide, delay);\n return this.loader;\n }\n // Remove loader\n this.loader.selector.remove();\n this.loader.selector = null;\n this.loader.content_selector = null;\n this.loader.progress_selector = null;\n this.loader.cancel_selector = null;\n this.loader.showing = false;\n return this.loader;\n }.bind(this)\n };\n return loader;\n};\n\n/**\n * Create a new subclass following classical inheritance patterns. Some registry singletons use this internally to\n * enable code reuse and customization of known LZ core functionality.\n *\n * @param {Function} parent A parent class constructor that will be extended by the child class\n * @param {Object} extra An object of additional properties and methods to add/override behavior for the child class\n * @param {Function} [new_constructor] An optional constructor function that performs additional setup. If omitted,\n * just calls the parent constructor by default. Implementer must manage super calls when overriding the constructor.\n * @returns {Function} The constructor for the new child class\n */\nLocusZoom.subclass = function(parent, extra, new_constructor) {\n if (typeof parent !== \"function\" ) {\n throw \"Parent must be a callable constructor\";\n }\n\n extra = extra || {};\n var Sub = new_constructor || function() {\n parent.apply(this, arguments);\n };\n\n Sub.prototype = Object.create(parent.prototype);\n Object.keys(extra).forEach(function(k) {\n Sub.prototype[k] = extra[k];\n });\n Sub.prototype.constructor = Sub;\n\n return Sub;\n};\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * Manage known layouts for all parts of the LocusZoom plot\n *\n * This registry allows for layouts to be reused and customized many times on a page, using a common base pattern.\n * It handles the work of ensuring that each new instance of the layout has no shared state with other copies.\n *\n * @class\n */\nLocusZoom.Layouts = (function() {\n var obj = {};\n var layouts = {\n \"plot\": {},\n \"panel\": {},\n \"data_layer\": {},\n \"dashboard\": {},\n \"tooltip\": {}\n };\n\n /**\n * Generate a layout configuration object\n * @param {('plot'|'panel'|'data_layer'|'dashboard'|'tooltip')} type The type of layout to retrieve\n * @param {string} name Identifier of the predefined layout within the specified type\n * @param {object} [modifications] Custom properties that override default settings for this layout\n * @returns {object} A JSON-serializable object representation\n */\n obj.get = function(type, name, modifications) {\n if (typeof type != \"string\" || typeof name != \"string\") {\n throw(\"invalid arguments passed to LocusZoom.Layouts.get, requires string (layout type) and string (layout name)\");\n } else if (layouts[type][name]) {\n // Get the base layout\n var layout = LocusZoom.Layouts.merge(modifications || {}, layouts[type][name]);\n // If \"unnamespaced\" is true then strike that from the layout and return the layout without namespacing\n if (layout.unnamespaced){\n delete layout.unnamespaced;\n return JSON.parse(JSON.stringify(layout));\n }\n // Determine the default namespace for namespaced values\n var default_namespace = \"\";\n if (typeof layout.namespace == \"string\"){\n default_namespace = layout.namespace;\n } else if (typeof layout.namespace == \"object\" && Object.keys(layout.namespace).length){\n if (typeof layout.namespace.default != \"undefined\"){\n default_namespace = layout.namespace.default;\n } else {\n default_namespace = layout.namespace[Object.keys(layout.namespace)[0]].toString();\n }\n }\n default_namespace += default_namespace.length ? \":\" : \"\";\n // Apply namespaces to layout, recursively\n var applyNamespaces = function(element, namespace){\n if (namespace){\n if (typeof namespace == \"string\"){\n namespace = { default: namespace }; \n }\n } else {\n namespace = { default: \"\" };\n }\n if (typeof element == \"string\"){\n var re = /\\{\\{namespace(\\[[A-Za-z_0-9]+\\]|)\\}\\}/g;\n var match, base, key, resolved_namespace;\n var replace = [];\n while ((match = re.exec(element)) !== null){\n base = match[0];\n key = match[1].length ? match[1].replace(/(\\[|\\])/g,\"\") : null;\n resolved_namespace = default_namespace;\n if (namespace != null && typeof namespace == \"object\" && typeof namespace[key] != \"undefined\"){\n resolved_namespace = namespace[key] + (namespace[key].length ? \":\" : \"\");\n }\n replace.push({ base: base, namespace: resolved_namespace });\n }\n for (var r in replace){\n element = element.replace(replace[r].base, replace[r].namespace);\n }\n } else if (typeof element == \"object\" && element != null){\n if (typeof element.namespace != \"undefined\"){\n var merge_namespace = (typeof element.namespace == \"string\") ? { default: element.namespace } : element.namespace;\n namespace = LocusZoom.Layouts.merge(namespace, merge_namespace);\n }\n var namespaced_element, namespaced_property;\n for (var property in element) {\n if (property === \"namespace\"){ continue; }\n namespaced_element = applyNamespaces(element[property], namespace);\n namespaced_property = applyNamespaces(property, namespace);\n if (property !== namespaced_property){\n delete element[property];\n }\n element[namespaced_property] = namespaced_element;\n }\n }\n return element;\n };\n layout = applyNamespaces(layout, layout.namespace);\n // Return the layout as valid JSON only\n return JSON.parse(JSON.stringify(layout));\n } else {\n throw(\"layout type [\" + type + \"] name [\" + name + \"] not found\");\n }\n };\n\n /** @private */\n obj.set = function(type, name, layout) {\n if (typeof type != \"string\" || typeof name != \"string\" || typeof layout != \"object\"){\n throw (\"unable to set new layout; bad arguments passed to set()\");\n }\n if (!layouts[type]){\n layouts[type] = {};\n }\n if (layout){\n return (layouts[type][name] = JSON.parse(JSON.stringify(layout)));\n } else {\n delete layouts[type][name];\n return null;\n }\n };\n\n /**\n * Register a new layout definition by name.\n *\n * @param {string} type The type of layout to add. Usually, this will be one of the predefined LocusZoom types,\n * but if you pass a different name, this method will automatically create the new `type` bucket\n * @param {string} name The identifier of the newly added layout\n * @param {object} [layout] A JSON-serializable object containing configuration properties for this layout\n * @returns The JSON representation of the newly created layout\n */\n obj.add = function(type, name, layout) {\n return obj.set(type, name, layout);\n };\n\n /**\n * List all registered layouts\n * @param [type] Optionally narrow the list to only layouts of a specific type; else return all known layouts\n * @returns {*}\n */\n obj.list = function(type) {\n if (!layouts[type]){\n var list = {};\n Object.keys(layouts).forEach(function(type){\n list[type] = Object.keys(layouts[type]);\n });\n return list;\n } else {\n return Object.keys(layouts[type]);\n }\n };\n\n /**\n * A helper method used for merging two objects. If a key is present in both, takes the value from the first object\n * Values from `default_layout` will be cleanly copied over, ensuring no references or shared state.\n *\n * Frequently used for preparing custom layouts. Both objects should be JSON-serializable.\n *\n * @param {object} custom_layout An object containing configuration parameters that override or add to defaults\n * @param {object} default_layout An object containing default settings.\n * @returns The custom layout is modified in place and also returned from this method.\n */\n obj.merge = function (custom_layout, default_layout) {\n if (typeof custom_layout !== \"object\" || typeof default_layout !== \"object\"){\n throw(\"LocusZoom.Layouts.merge only accepts two layout objects; \" + (typeof custom_layout) + \", \" + (typeof default_layout) + \" given\");\n }\n for (var property in default_layout) {\n if (!default_layout.hasOwnProperty(property)){ continue; }\n // Get types for comparison. Treat nulls in the custom layout as undefined for simplicity.\n // (javascript treats nulls as \"object\" when we just want to overwrite them as if they're undefined)\n // Also separate arrays from objects as a discrete type.\n var custom_type = custom_layout[property] === null ? \"undefined\" : typeof custom_layout[property];\n var default_type = typeof default_layout[property];\n if (custom_type === \"object\" && Array.isArray(custom_layout[property])){ custom_type = \"array\"; }\n if (default_type === \"object\" && Array.isArray(default_layout[property])){ default_type = \"array\"; }\n // Unsupported property types: throw an exception\n if (custom_type === \"function\" || default_type === \"function\"){\n throw(\"LocusZoom.Layouts.merge encountered an unsupported property type\");\n }\n // Undefined custom value: pull the default value\n if (custom_type === \"undefined\"){\n custom_layout[property] = JSON.parse(JSON.stringify(default_layout[property]));\n continue;\n }\n // Both values are objects: merge recursively\n if (custom_type === \"object\" && default_type === \"object\"){\n custom_layout[property] = LocusZoom.Layouts.merge(custom_layout[property], default_layout[property]);\n continue;\n }\n }\n return custom_layout;\n };\n\n return obj;\n})();\n\n\n/**\n * Tooltip Layouts\n * @namespace LocusZoom.Layouts.tooltips\n */\n\n// TODO: Improve documentation of predefined types within layout namespaces\nLocusZoom.Layouts.add(\"tooltip\", \"standard_association\", {\n namespace: { \"assoc\": \"assoc\" },\n closable: true,\n show: { or: [\"highlighted\", \"selected\"] },\n hide: { and: [\"unhighlighted\", \"unselected\"] },\n html: \"{{{{namespace[assoc]}}variant}}
\"\n + \"P Value: {{{{namespace[assoc]}}log_pvalue|logtoscinotation}}
\"\n + \"Ref. Allele: {{{{namespace[assoc]}}ref_allele}}
\"\n + \"Make LD Reference
\"\n});\n\nvar covariates_model_association = LocusZoom.Layouts.get(\"tooltip\", \"standard_association\", { unnamespaced: true });\ncovariates_model_association.html += \"Condition on Variant
\";\nLocusZoom.Layouts.add(\"tooltip\", \"covariates_model_association\", covariates_model_association);\n\nLocusZoom.Layouts.add(\"tooltip\", \"standard_genes\", {\n closable: true,\n show: { or: [\"highlighted\", \"selected\"] },\n hide: { and: [\"unhighlighted\", \"unselected\"] },\n html: \"

{{gene_name}}

\"\n + \"
Gene ID: {{gene_id}}
\"\n + \"
Transcript ID: {{transcript_id}}
\"\n + \"
\"\n + \"\"\n + \"\"\n + \"\"\n + \"\"\n + \"\"\n + \"
ConstraintExpected variantsObserved variantsConst. Metric
Synonymous{{exp_syn}}{{n_syn}}z = {{syn_z}}
Missense{{exp_mis}}{{n_mis}}z = {{mis_z}}
LoF{{exp_lof}}{{n_lof}}pLI = {{pLI}}
\"\n + \"More data on ExAC\"\n});\n\nLocusZoom.Layouts.add(\"tooltip\", \"standard_intervals\", {\n namespace: { \"intervals\": \"intervals\" },\n closable: false,\n show: { or: [\"highlighted\", \"selected\"] },\n hide: { and: [\"unhighlighted\", \"unselected\"] },\n html: \"{{{{namespace[intervals]}}state_name}}
{{{{namespace[intervals]}}start}}-{{{{namespace[intervals]}}end}}\"\n});\n\n/**\n * Data Layer Layouts: represent specific information from a data source\n * @namespace Layouts.data_layer\n*/\n\nLocusZoom.Layouts.add(\"data_layer\", \"significance\", {\n id: \"significance\",\n type: \"orthogonal_line\",\n orientation: \"horizontal\",\n offset: 4.522\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"recomb_rate\", {\n namespace: { \"recomb\": \"recomb\" },\n id: \"recombrate\",\n type: \"line\",\n fields: [\"{{namespace[recomb]}}position\", \"{{namespace[recomb]}}recomb_rate\"],\n z_index: 1,\n style: {\n \"stroke\": \"#0000FF\",\n \"stroke-width\": \"1.5px\"\n },\n x_axis: {\n field: \"{{namespace[recomb]}}position\"\n },\n y_axis: {\n axis: 2,\n field: \"{{namespace[recomb]}}recomb_rate\",\n floor: 0,\n ceiling: 100\n }\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"association_pvalues\", {\n namespace: { \"assoc\": \"assoc\", \"ld\": \"ld\" },\n id: \"associationpvalues\",\n type: \"scatter\",\n point_shape: {\n scale_function: \"if\",\n field: \"{{namespace[ld]}}isrefvar\",\n parameters: {\n field_value: 1,\n then: \"diamond\",\n else: \"circle\"\n }\n },\n point_size: {\n scale_function: \"if\",\n field: \"{{namespace[ld]}}isrefvar\",\n parameters: {\n field_value: 1,\n then: 80,\n else: 40\n }\n },\n color: [\n {\n scale_function: \"if\",\n field: \"{{namespace[ld]}}isrefvar\",\n parameters: {\n field_value: 1,\n then: \"#9632b8\"\n }\n },\n {\n scale_function: \"numerical_bin\",\n field: \"{{namespace[ld]}}state\",\n parameters: {\n breaks: [0, 0.2, 0.4, 0.6, 0.8],\n values: [\"#357ebd\",\"#46b8da\",\"#5cb85c\",\"#eea236\",\"#d43f3a\"]\n }\n },\n \"#B8B8B8\"\n ],\n legend: [\n { shape: \"diamond\", color: \"#9632b8\", size: 40, label: \"LD Ref Var\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#d43f3a\", size: 40, label: \"1.0 > r² ≥ 0.8\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#eea236\", size: 40, label: \"0.8 > r² ≥ 0.6\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#5cb85c\", size: 40, label: \"0.6 > r² ≥ 0.4\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#46b8da\", size: 40, label: \"0.4 > r² ≥ 0.2\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#357ebd\", size: 40, label: \"0.2 > r² ≥ 0.0\", class: \"lz-data_layer-scatter\" },\n { shape: \"circle\", color: \"#B8B8B8\", size: 40, label: \"no r² data\", class: \"lz-data_layer-scatter\" }\n ],\n fields: [\"{{namespace[assoc]}}variant\", \"{{namespace[assoc]}}position\", \"{{namespace[assoc]}}log_pvalue\", \"{{namespace[assoc]}}log_pvalue|logtoscinotation\", \"{{namespace[assoc]}}ref_allele\", \"{{namespace[ld]}}state\", \"{{namespace[ld]}}isrefvar\"],\n id_field: \"{{namespace[assoc]}}variant\",\n z_index: 2,\n x_axis: {\n field: \"{{namespace[assoc]}}position\"\n },\n y_axis: {\n axis: 1,\n field: \"{{namespace[assoc]}}log_pvalue\",\n floor: 0,\n upper_buffer: 0.10,\n min_extent: [ 0, 10 ]\n },\n behaviors: {\n onmouseover: [\n { action: \"set\", status: \"highlighted\" }\n ],\n onmouseout: [\n { action: \"unset\", status: \"highlighted\" }\n ],\n onclick: [\n { action: \"toggle\", status: \"selected\", exclusive: true }\n ],\n onshiftclick: [\n { action: \"toggle\", status: \"selected\" }\n ]\n },\n tooltip: LocusZoom.Layouts.get(\"tooltip\", \"standard_association\", { unnamespaced: true })\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"phewas_pvalues\", {\n namespace: {\"phewas\": \"phewas\"},\n id: \"phewaspvalues\",\n type: \"category_scatter\",\n point_shape: \"circle\",\n point_size: 70,\n tooltip_positioning: \"vertical\",\n id_field: \"{{namespace[phewas]}}id\",\n fields: [\"{{namespace[phewas]}}id\", \"{{namespace[phewas]}}log_pvalue\", \"{{namespace[phewas]}}trait_group\", \"{{namespace[phewas]}}trait_label\"],\n x_axis: {\n field: \"{{namespace[phewas]}}x\", // Synthetic/derived field added by `category_scatter` layer\n category_field: \"{{namespace[phewas]}}trait_group\",\n lower_buffer: 0.025,\n upper_buffer: 0.025\n },\n y_axis: {\n axis: 1,\n field: \"{{namespace[phewas]}}log_pvalue\",\n floor: 0,\n upper_buffer: 0.15\n },\n color: {\n field: \"{{namespace[phewas]}}trait_group\",\n scale_function: \"categorical_bin\",\n parameters: {\n categories: [],\n values: [],\n null_value: \"#B8B8B8\"\n }\n },\n fill_opacity: 0.7,\n tooltip: {\n closable: true,\n show: { or: [\"highlighted\", \"selected\"] },\n hide: { and: [\"unhighlighted\", \"unselected\"] },\n html: [\n \"Trait: {{{{namespace[phewas]}}trait_label|htmlescape}}
\",\n \"Trait Category: {{{{namespace[phewas]}}trait_group|htmlescape}}
\",\n \"P-value: {{{{namespace[phewas]}}log_pvalue|logtoscinotation|htmlescape}}
\"\n ].join(\"\")\n },\n behaviors: {\n onmouseover: [\n { action: \"set\", status: \"highlighted\" }\n ],\n onmouseout: [\n { action: \"unset\", status: \"highlighted\" }\n ],\n onclick: [\n { action: \"toggle\", status: \"selected\", exclusive: true }\n ],\n onshiftclick: [\n { action: \"toggle\", status: \"selected\" }\n ]\n },\n label: {\n text: \"{{{{namespace[phewas]}}trait_label}}\",\n spacing: 6,\n lines: {\n style: {\n \"stroke-width\": \"2px\",\n \"stroke\": \"#333333\",\n \"stroke-dasharray\": \"2px 2px\"\n }\n },\n filters: [\n {\n field: \"{{namespace[phewas]}}log_pvalue\",\n operator: \">=\",\n value: 20\n }\n ],\n style: {\n \"font-size\": \"14px\",\n \"font-weight\": \"bold\",\n \"fill\": \"#333333\"\n }\n }\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"genes\", {\n namespace: { \"gene\": \"gene\", \"constraint\": \"constraint\" },\n id: \"genes\",\n type: \"genes\",\n fields: [\"{{namespace[gene]}}gene\", \"{{namespace[constraint]}}constraint\"],\n id_field: \"gene_id\",\n behaviors: {\n onmouseover: [\n { action: \"set\", status: \"highlighted\" }\n ],\n onmouseout: [\n { action: \"unset\", status: \"highlighted\" }\n ],\n onclick: [\n { action: \"toggle\", status: \"selected\", exclusive: true }\n ],\n onshiftclick: [\n { action: \"toggle\", status: \"selected\" }\n ]\n },\n tooltip: LocusZoom.Layouts.get(\"tooltip\", \"standard_genes\", { unnamespaced: true })\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"genome_legend\", {\n namespace: { \"genome\": \"genome\" },\n id: \"genome_legend\",\n type: \"genome_legend\",\n fields: [\"{{namespace[genome]}}chr\", \"{{namespace[genome]}}base_pairs\"],\n x_axis: {\n floor: 0,\n ceiling: 2881033286\n }\n});\n\nLocusZoom.Layouts.add(\"data_layer\", \"intervals\", {\n namespace: { \"intervals\": \"intervals\" },\n id: \"intervals\",\n type: \"intervals\",\n fields: [\"{{namespace[intervals]}}start\",\"{{namespace[intervals]}}end\",\"{{namespace[intervals]}}state_id\",\"{{namespace[intervals]}}state_name\"],\n id_field: \"{{namespace[intervals]}}start\",\n start_field: \"{{namespace[intervals]}}start\",\n end_field: \"{{namespace[intervals]}}end\",\n track_split_field: \"{{namespace[intervals]}}state_id\",\n split_tracks: true,\n always_hide_legend: false,\n color: {\n field: \"{{namespace[intervals]}}state_id\",\n scale_function: \"categorical_bin\",\n parameters: {\n categories: [1,2,3,4,5,6,7,8,9,10,11,12,13],\n values: [\"rgb(212,63,58)\", \"rgb(250,120,105)\", \"rgb(252,168,139)\", \"rgb(240,189,66)\", \"rgb(250,224,105)\", \"rgb(240,238,84)\", \"rgb(244,252,23)\", \"rgb(23,232,252)\", \"rgb(32,191,17)\", \"rgb(23,166,77)\", \"rgb(32,191,17)\", \"rgb(162,133,166)\", \"rgb(212,212,212)\"],\n null_value: \"#B8B8B8\"\n }\n },\n legend: [\n { shape: \"rect\", color: \"rgb(212,63,58)\", width: 9, label: \"Active Promoter\", \"{{namespace[intervals]}}state_id\": 1 },\n { shape: \"rect\", color: \"rgb(250,120,105)\", width: 9, label: \"Weak Promoter\", \"{{namespace[intervals]}}state_id\": 2 },\n { shape: \"rect\", color: \"rgb(252,168,139)\", width: 9, label: \"Poised Promoter\", \"{{namespace[intervals]}}state_id\": 3 },\n { shape: \"rect\", color: \"rgb(240,189,66)\", width: 9, label: \"Strong enhancer\", \"{{namespace[intervals]}}state_id\": 4 },\n { shape: \"rect\", color: \"rgb(250,224,105)\", width: 9, label: \"Strong enhancer\", \"{{namespace[intervals]}}state_id\": 5 },\n { shape: \"rect\", color: \"rgb(240,238,84)\", width: 9, label: \"Weak enhancer\", \"{{namespace[intervals]}}state_id\": 6 },\n { shape: \"rect\", color: \"rgb(244,252,23)\", width: 9, label: \"Weak enhancer\", \"{{namespace[intervals]}}state_id\": 7 },\n { shape: \"rect\", color: \"rgb(23,232,252)\", width: 9, label: \"Insulator\", \"{{namespace[intervals]}}state_id\": 8 },\n { shape: \"rect\", color: \"rgb(32,191,17)\", width: 9, label: \"Transcriptional transition\", \"{{namespace[intervals]}}state_id\": 9 },\n { shape: \"rect\", color: \"rgb(23,166,77)\", width: 9, label: \"Transcriptional elongation\", \"{{namespace[intervals]}}state_id\": 10 },\n { shape: \"rect\", color: \"rgb(136,240,129)\", width: 9, label: \"Weak transcribed\", \"{{namespace[intervals]}}state_id\": 11 },\n { shape: \"rect\", color: \"rgb(162,133,166)\", width: 9, label: \"Polycomb-repressed\", \"{{namespace[intervals]}}state_id\": 12 },\n { shape: \"rect\", color: \"rgb(212,212,212)\", width: 9, label: \"Heterochromatin / low signal\", \"{{namespace[intervals]}}state_id\": 13 }\n ],\n behaviors: {\n onmouseover: [\n { action: \"set\", status: \"highlighted\" }\n ],\n onmouseout: [\n { action: \"unset\", status: \"highlighted\" }\n ],\n onclick: [\n { action: \"toggle\", status: \"selected\", exclusive: true }\n ],\n onshiftclick: [\n { action: \"toggle\", status: \"selected\" }\n ]\n },\n tooltip: LocusZoom.Layouts.get(\"tooltip\", \"standard_intervals\", { unnamespaced: true })\n});\n\n/**\n * Dashboard Layouts: toolbar buttons etc\n * @namespace Layouts.dashboard\n */\nLocusZoom.Layouts.add(\"dashboard\", \"standard_panel\", {\n components: [\n {\n type: \"remove_panel\",\n position: \"right\",\n color: \"red\",\n group_position: \"end\"\n },\n {\n type: \"move_panel_up\",\n position: \"right\",\n group_position: \"middle\"\n },\n {\n type: \"move_panel_down\",\n position: \"right\",\n group_position: \"start\",\n style: { \"margin-left\": \"0.75em\" }\n }\n ]\n}); \n\nLocusZoom.Layouts.add(\"dashboard\", \"standard_plot\", {\n components: [\n {\n type: \"title\",\n title: \"LocusZoom\",\n subtitle: \"v\" + LocusZoom.version + \"\",\n position: \"left\"\n },\n {\n type: \"dimensions\",\n position: \"right\"\n },\n {\n type: \"region_scale\",\n position: \"right\"\n },\n {\n type: \"download\",\n position: \"right\"\n }\n ]\n});\n\nvar covariates_model_plot_dashboard = LocusZoom.Layouts.get(\"dashboard\", \"standard_plot\");\ncovariates_model_plot_dashboard.components.push({\n type: \"covariates_model\",\n button_html: \"Model\",\n button_title: \"Show and edit covariates currently in model\",\n position: \"left\"\n});\nLocusZoom.Layouts.add(\"dashboard\", \"covariates_model_plot\", covariates_model_plot_dashboard);\n\nvar region_nav_plot_dashboard = LocusZoom.Layouts.get(\"dashboard\", \"standard_plot\");\nregion_nav_plot_dashboard.components.push({\n type: \"shift_region\",\n step: 500000,\n button_html: \">>\",\n position: \"right\",\n group_position: \"end\"\n});\nregion_nav_plot_dashboard.components.push({\n type: \"shift_region\",\n step: 50000,\n button_html: \">\",\n position: \"right\",\n group_position: \"middle\"\n});\nregion_nav_plot_dashboard.components.push({\n type: \"zoom_region\",\n step: 0.2,\n position: \"right\",\n group_position: \"middle\"\n});\nregion_nav_plot_dashboard.components.push({\n type: \"zoom_region\",\n step: -0.2,\n position: \"right\",\n group_position: \"middle\"\n});\nregion_nav_plot_dashboard.components.push({\n type: \"shift_region\",\n step: -50000,\n button_html: \"<\",\n position: \"right\",\n group_position: \"middle\"\n});\nregion_nav_plot_dashboard.components.push({\n type: \"shift_region\",\n step: -500000,\n button_html: \"<<\",\n position: \"right\",\n group_position: \"start\"\n});\nLocusZoom.Layouts.add(\"dashboard\", \"region_nav_plot\", region_nav_plot_dashboard);\n\n/**\n * Panel Layouts\n * @namespace Layouts.panel\n */\n\nLocusZoom.Layouts.add(\"panel\", \"association\", {\n id: \"association\",\n width: 800,\n height: 225,\n min_width: 400,\n min_height: 200,\n proportional_width: 1,\n margin: { top: 35, right: 50, bottom: 40, left: 50 },\n inner_border: \"rgb(210, 210, 210)\",\n dashboard: (function(){\n var l = LocusZoom.Layouts.get(\"dashboard\", \"standard_panel\", { unnamespaced: true });\n l.components.push({\n type: \"toggle_legend\",\n position: \"right\"\n });\n return l;\n })(),\n axes: {\n x: {\n label: \"Chromosome {{chr}} (Mb)\",\n label_offset: 32,\n tick_format: \"region\",\n extent: \"state\"\n },\n y1: {\n label: \"-log10 p-value\",\n label_offset: 28\n },\n y2: {\n label: \"Recombination Rate (cM/Mb)\",\n label_offset: 40\n }\n },\n legend: {\n orientation: \"vertical\",\n origin: { x: 55, y: 40 },\n hidden: true\n },\n interaction: {\n drag_background_to_pan: true,\n drag_x_ticks_to_scale: true,\n drag_y1_ticks_to_scale: true,\n drag_y2_ticks_to_scale: true,\n scroll_to_zoom: true,\n x_linked: true\n },\n data_layers: [\n LocusZoom.Layouts.get(\"data_layer\", \"significance\", { unnamespaced: true }),\n LocusZoom.Layouts.get(\"data_layer\", \"recomb_rate\", { unnamespaced: true }),\n LocusZoom.Layouts.get(\"data_layer\", \"association_pvalues\", { unnamespaced: true })\n ]\n});\n\nLocusZoom.Layouts.add(\"panel\", \"genes\", {\n id: \"genes\",\n width: 800,\n height: 225,\n min_width: 400,\n min_height: 112.5,\n proportional_width: 1,\n margin: { top: 20, right: 50, bottom: 20, left: 50 },\n axes: {},\n interaction: {\n drag_background_to_pan: true,\n scroll_to_zoom: true,\n x_linked: true\n },\n dashboard: (function(){\n var l = LocusZoom.Layouts.get(\"dashboard\", \"standard_panel\", { unnamespaced: true });\n l.components.push({\n type: \"resize_to_data\",\n position: \"right\"\n });\n return l;\n })(), \n data_layers: [\n LocusZoom.Layouts.get(\"data_layer\", \"genes\", { unnamespaced: true })\n ]\n});\n\nLocusZoom.Layouts.add(\"panel\", \"phewas\", {\n id: \"phewas\",\n width: 800,\n height: 300,\n min_width: 800,\n min_height: 300,\n proportional_width: 1,\n margin: { top: 20, right: 50, bottom: 120, left: 50 },\n inner_border: \"rgb(210, 210, 210)\",\n axes: {\n x: {\n ticks: { // Object based config (shared defaults; allow layers to specify ticks)\n style: {\n \"font-weight\": \"bold\",\n \"font-size\": \"11px\",\n \"text-anchor\": \"start\"\n },\n transform: \"rotate(50)\",\n position: \"left\" // Special param recognized by `category_scatter` layers\n }\n },\n y1: {\n label: \"-log10 p-value\",\n label_offset: 28\n }\n },\n data_layers: [\n LocusZoom.Layouts.get(\"data_layer\", \"significance\", { unnamespaced: true }),\n LocusZoom.Layouts.get(\"data_layer\", \"phewas_pvalues\", { unnamespaced: true })\n ]\n});\n\nLocusZoom.Layouts.add(\"panel\", \"genome_legend\", {\n id: \"genome_legend\",\n width: 800,\n height: 50,\n origin: { x: 0, y: 300 },\n min_width: 800,\n min_height: 50,\n proportional_width: 1,\n margin: { top: 0, right: 50, bottom: 35, left: 50 },\n axes: {\n x: {\n label: \"Genomic Position (number denotes chromosome)\",\n label_offset: 35,\n ticks: [\n {\n x: 124625310,\n text: \"1\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 370850307,\n text: \"2\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 591461209,\n text: \"3\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 786049562,\n text: \"4\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 972084330,\n text: \"5\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1148099493,\n text: \"6\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1313226358,\n text: \"7\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1465977701,\n text: \"8\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1609766427,\n text: \"9\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1748140516,\n text: \"10\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 1883411148,\n text: \"11\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2017840353,\n text: \"12\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2142351240,\n text: \"13\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2253610949,\n text: \"14\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2358551415,\n text: \"15\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2454994487,\n text: \"16\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2540769469,\n text: \"17\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2620405698,\n text: \"18\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2689008813,\n text: \"19\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2750086065,\n text: \"20\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2805663772,\n text: \"21\",\n style: {\n \"fill\": \"rgb(120, 120, 186)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n },\n {\n x: 2855381003,\n text: \"22\",\n style: {\n \"fill\": \"rgb(0, 0, 66)\",\n \"text-anchor\": \"center\",\n \"font-size\": \"13px\",\n \"font-weight\": \"bold\"\n },\n transform: \"translate(0, 2)\"\n }\n ]\n }\n },\n data_layers: [\n LocusZoom.Layouts.get(\"data_layer\", \"genome_legend\", { unnamespaced: true })\n ]\n});\n\nLocusZoom.Layouts.add(\"panel\", \"intervals\", {\n id: \"intervals\",\n width: 1000,\n height: 50,\n min_width: 500,\n min_height: 50,\n margin: { top: 25, right: 150, bottom: 5, left: 50 },\n dashboard: (function(){\n var l = LocusZoom.Layouts.get(\"dashboard\", \"standard_panel\", { unnamespaced: true });\n l.components.push({\n type: \"toggle_split_tracks\",\n data_layer_id: \"intervals\",\n position: \"right\"\n });\n return l;\n })(),\n axes: {},\n interaction: {\n drag_background_to_pan: true,\n scroll_to_zoom: true,\n x_linked: true\n },\n legend: {\n hidden: true,\n orientation: \"horizontal\",\n origin: { x: 50, y: 0 },\n pad_from_bottom: 5\n },\n data_layers: [\n LocusZoom.Layouts.get(\"data_layer\", \"intervals\", { unnamespaced: true })\n ]\n});\n\n\n/**\n * Plot Layouts\n * @namespace Layouts.plot\n */\n\nLocusZoom.Layouts.add(\"plot\", \"standard_association\", {\n state: {},\n width: 800,\n height: 450,\n responsive_resize: true,\n min_region_scale: 20000,\n max_region_scale: 1000000,\n dashboard: LocusZoom.Layouts.get(\"dashboard\", \"standard_plot\", { unnamespaced: true }),\n panels: [\n LocusZoom.Layouts.get(\"panel\", \"association\", { unnamespaced: true, proportional_height: 0.5 }),\n LocusZoom.Layouts.get(\"panel\", \"genes\", { unnamespaced: true, proportional_height: 0.5 })\n ]\n});\n\n// Shortcut to \"StandardLayout\" for backward compatibility\nLocusZoom.StandardLayout = LocusZoom.Layouts.get(\"plot\", \"standard_association\");\n\nLocusZoom.Layouts.add(\"plot\", \"standard_phewas\", {\n width: 800,\n height: 600,\n min_width: 800,\n min_height: 600,\n responsive_resize: true,\n dashboard: LocusZoom.Layouts.get(\"dashboard\", \"standard_plot\", { unnamespaced: true } ),\n panels: [\n LocusZoom.Layouts.get(\"panel\", \"phewas\", { unnamespaced: true, proportional_height: 0.45 }),\n LocusZoom.Layouts.get(\"panel\", \"genome_legend\", { unnamespaced: true, proportional_height: 0.1 }),\n LocusZoom.Layouts.get(\"panel\", \"genes\", {\n unnamespaced: true, proportional_height: 0.45,\n margin: { bottom: 40 },\n axes: {\n x: {\n label: \"Chromosome {{chr}} (Mb)\",\n label_offset: 32,\n tick_format: \"region\",\n extent: \"state\"\n }\n }\n })\n ],\n mouse_guide: false\n});\n\nLocusZoom.Layouts.add(\"plot\", \"interval_association\", {\n state: {},\n width: 800,\n height: 550,\n responsive_resize: true,\n min_region_scale: 20000,\n max_region_scale: 1000000,\n dashboard: LocusZoom.Layouts.get(\"dashboard\", \"standard_plot\", { unnamespaced: true }),\n panels: [\n LocusZoom.Layouts.get(\"panel\", \"association\", { unnamespaced: true, width: 800, proportional_height: (225/570) }),\n LocusZoom.Layouts.get(\"panel\", \"intervals\", { unnamespaced: true, proportional_height: (120/570) }),\n LocusZoom.Layouts.get(\"panel\", \"genes\", { unnamespaced: true, width: 800, proportional_height: (225/570) })\n ]\n});\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * A data layer is an abstract class representing a data set and its graphical representation within a panel\n * @public\n * @class\n * @param {Object} layout A JSON-serializable object describing the layout for this layer\n * @param {LocusZoom.DataLayer|LocusZoom.Panel} parent Where this layout is used\n*/\nLocusZoom.DataLayer = function(layout, parent) {\n /** @member {Boolean} */\n this.initialized = false;\n /** @member {Number} */\n this.layout_idx = null;\n\n /** @member {String} */\n this.id = null;\n /** @member {LocusZoom.Panel} */\n this.parent = parent || null;\n /**\n * @member {{group: d3.selection, container: d3.selection, clipRect: d3.selection}}\n */\n this.svg = {};\n\n /** @member {LocusZoom.Plot} */\n this.parent_plot = null;\n if (typeof parent != \"undefined\" && parent instanceof LocusZoom.Panel){ this.parent_plot = parent.parent; }\n\n /** @member {Object} */\n this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.DataLayer.DefaultLayout);\n if (this.layout.id){ this.id = this.layout.id; }\n\n // Ensure any axes defined in the layout have an explicit axis number (default: 1)\n if (this.layout.x_axis !== {} && typeof this.layout.x_axis.axis !== \"number\"){ this.layout.x_axis.axis = 1; }\n if (this.layout.y_axis !== {} && typeof this.layout.y_axis.axis !== \"number\"){ this.layout.y_axis.axis = 1; }\n\n /**\n * Values in the layout object may change during rendering etc. Retain a copy of the original data layer state\n * @member {Object}\n */\n this._base_layout = JSON.parse(JSON.stringify(this.layout));\n\n /** @member {Object} */\n this.state = {};\n /** @member {String} */\n this.state_id = null;\n\n this.setDefaultState();\n\n // Initialize parameters for storing data and tool tips\n /** @member {Array} */\n this.data = [];\n if (this.layout.tooltip){\n /** @member {Object} */\n this.tooltips = {};\n }\n\n // Initialize flags for tracking global statuses\n this.global_statuses = {\n \"highlighted\": false,\n \"selected\": false,\n \"faded\": false,\n \"hidden\": false\n };\n \n return this;\n\n};\n\n/**\n * Instruct this datalayer to begin tracking additional fields from data sources (does not guarantee that such a field actually exists)\n *\n * Custom plots can use this to dynamically extend datalayer functionality after the plot is drawn\n *\n * (since removing core fields may break layer functionality, there is presently no hook for the inverse behavior)\n * @param fieldName\n * @param namespace\n * @param {String|String[]} transformations The name (or array of names) of transformations to apply to this field\n * @returns {String} The raw string added to the fields array\n */\nLocusZoom.DataLayer.prototype.addField = function(fieldName, namespace, transformations) {\n if (!fieldName || !namespace) {\n throw \"Must specify field name and namespace to use when adding field\";\n }\n var fieldString = namespace + \":\" + fieldName;\n if (transformations) {\n fieldString += \"|\";\n if (typeof transformations === \"string\") {\n fieldString += transformations;\n } else if (Array.isArray(transformations)) {\n fieldString += transformations.join(\"|\");\n } else {\n throw \"Must provide transformations as either a string or array of strings\";\n }\n }\n var fields = this.layout.fields;\n if (fields.indexOf(fieldString) === -1) {\n fields.push(fieldString);\n }\n return fieldString;\n};\n\n/**\n * Define default state that should get tracked during the lifetime of this layer.\n *\n * In some special custom usages, it may be useful to completely reset a panel (eg \"click for\n * genome region\" links), plotting new data that invalidates any previously tracked state. This hook makes it\n * possible to reset without destroying the panel entirely. It is used by `Plot.clearPanelData`.\n */\nLocusZoom.DataLayer.prototype.setDefaultState = function() {\n // Define state parameters specific to this data layer\n if (this.parent){\n this.state = this.parent.state;\n this.state_id = this.parent.id + \".\" + this.id;\n this.state[this.state_id] = this.state[this.state_id] || {};\n LocusZoom.DataLayer.Statuses.adjectives.forEach(function(status){\n this.state[this.state_id][status] = this.state[this.state_id][status] || [];\n }.bind(this));\n }\n};\n\n/**\n * A basic description of keys expected in a layout. Not intended to be directly used or modified by an end user.\n * @protected\n * @type {{type: string, fields: Array, x_axis: {}, y_axis: {}}}\n */\nLocusZoom.DataLayer.DefaultLayout = {\n type: \"\",\n fields: [],\n x_axis: {},\n y_axis: {}\n};\n\n/**\n * Available statuses that individual elements can have. Each status is described by\n * a verb/antiverb and an adjective. Verbs and antiverbs are used to generate data layer\n * methods for updating the status on one or more elements. Adjectives are used in class\n * names and applied or removed from elements to have a visual representation of the status,\n * as well as used as keys in the state for tracking which elements are in which status(es)\n * @static\n * @type {{verbs: String[], adjectives: String[], menu_antiverbs: String[]}}\n */\nLocusZoom.DataLayer.Statuses = {\n verbs: [\"highlight\", \"select\", \"fade\", \"hide\"],\n adjectives: [\"highlighted\", \"selected\", \"faded\", \"hidden\"],\n menu_antiverbs: [\"unhighlight\", \"deselect\", \"unfade\", \"show\"]\n};\n\n/**\n * Get the fully qualified identifier for the data layer, prefixed by any parent or container elements\n *\n * @returns {string} A dot-delimited string of the format ..\n */\nLocusZoom.DataLayer.prototype.getBaseId = function(){\n return this.parent_plot.id + \".\" + this.parent.id + \".\" + this.id;\n};\n\n/**\n * Determine the pixel height of data-bound objects represented inside this data layer. (excluding elements such as axes)\n *\n * May be used by operations that resize the data layer to fit available data\n *\n * @public\n * @returns {number}\n */\nLocusZoom.DataLayer.prototype.getAbsoluteDataHeight = function(){\n var dataBCR = this.svg.group.node().getBoundingClientRect();\n return dataBCR.height;\n};\n\n/**\n * Whether transitions can be applied to this data layer\n * @returns {boolean}\n */\nLocusZoom.DataLayer.prototype.canTransition = function(){\n if (!this.layout.transition){ return false; }\n return !(this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.panel_id);\n};\n\n/**\n * Fetch the fully qualified ID to be associated with a specific visual element, based on the data to which that\n * element is bound. In general this element ID will be unique, allowing it to be addressed directly via selectors.\n * @param {String|Object} element\n * @returns {String}\n */\nLocusZoom.DataLayer.prototype.getElementId = function(element){\n var element_id = \"element\";\n if (typeof element == \"string\"){\n element_id = element;\n } else if (typeof element == \"object\"){\n var id_field = this.layout.id_field || \"id\";\n if (typeof element[id_field] == \"undefined\"){\n throw(\"Unable to generate element ID\");\n }\n element_id = element[id_field].toString().replace(/\\W/g,\"\");\n }\n return (this.getBaseId() + \"-\" + element_id).replace(/(:|\\.|\\[|\\]|,)/g, \"_\");\n};\n\n/**\n * Fetch an ID that may bind a data element to a separate visual node for displaying status\n * Examples of this might be seperate visual nodes to show select/highlight statuses, or\n * even a common/shared node to show status across many elements in a set.\n * Abstract method. It should be overridden by data layers that implement seperate status\n * nodes specifically to the use case of the data layer type.\n * @param {String|Object} element\n * @returns {String|null}\n */\nLocusZoom.DataLayer.prototype.getElementStatusNodeId = function(element){\n return null;\n};\n\n/**\n * Returns a reference to the underlying data associated with a single visual element in the data layer, as\n * referenced by the unique identifier for the element\n\n * @param {String} id The unique identifier for the element, as defined by `getElementId`\n * @returns {Object|null} The data bound to that element\n */\nLocusZoom.DataLayer.prototype.getElementById = function(id){\n var selector = d3.select(\"#\" + id.replace(/(:|\\.|\\[|\\]|,)/g, \"\\\\$1\"));\n if (!selector.empty() && selector.data() && selector.data().length){\n return selector.data()[0];\n } else {\n return null;\n }\n};\n\n/**\n * Basic method to apply arbitrary methods and properties to data elements.\n * This is called on all data immediately after being fetched.\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.applyDataMethods = function(){\n this.data.forEach(function(d, i){\n // Basic toHTML() method - return the stringified value in the id_field, if defined.\n this.data[i].toHTML = function(){\n var id_field = this.layout.id_field || \"id\";\n var html = \"\";\n if (this.data[i][id_field]){ html = this.data[i][id_field].toString(); }\n return html;\n }.bind(this);\n // getDataLayer() method - return a reference to the data layer\n this.data[i].getDataLayer = function(){\n return this;\n }.bind(this);\n // deselect() method - shortcut method to deselect the element\n this.data[i].deselect = function(){\n var data_layer = this.getDataLayer();\n data_layer.unselectElement(this);\n };\n }.bind(this));\n this.applyCustomDataMethods();\n return this;\n};\n\n/**\n * Hook that allows custom datalayers to apply additional methods and properties to data elements as needed\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.applyCustomDataMethods = function(){\n return this;\n};\n\n/**\n * Initialize a data layer\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.initialize = function(){\n\n // Append a container group element to house the main data layer group element and the clip path\n this.svg.container = this.parent.svg.group.append(\"g\")\n .attr(\"class\", \"lz-data_layer-container\")\n .attr(\"id\", this.getBaseId() + \".data_layer_container\");\n \n // Append clip path to the container element\n this.svg.clipRect = this.svg.container.append(\"clipPath\")\n .attr(\"id\", this.getBaseId() + \".clip\")\n .append(\"rect\");\n \n // Append svg group for rendering all data layer elements, clipped by the clip path\n this.svg.group = this.svg.container.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".data_layer\")\n .attr(\"clip-path\", \"url(#\" + this.getBaseId() + \".clip)\");\n\n return this;\n\n};\n\n/**\n * Move a data layer up relative to others by z-index\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.moveUp = function(){\n if (this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1]){\n this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1];\n this.parent.data_layer_ids_by_z_index[this.layout.z_index + 1] = this.id;\n this.parent.resortDataLayers();\n }\n return this;\n};\n\n/**\n * Move a data layer down relative to others by z-index\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.moveDown = function(){\n if (this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1]){\n this.parent.data_layer_ids_by_z_index[this.layout.z_index] = this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1];\n this.parent.data_layer_ids_by_z_index[this.layout.z_index - 1] = this.id;\n this.parent.resortDataLayers();\n }\n return this;\n};\n\n/**\n * Apply scaling functions to an element or parameter as needed, based on its layout and the element's data\n * If the layout parameter is already a primitive type, simply return the value as given\n * @param {Array|Number|String|Object} layout\n * @param {*} data The value to be used with the filter\n * @returns {*} The transformed value\n */\nLocusZoom.DataLayer.prototype.resolveScalableParameter = function(layout, data){\n var ret = null;\n if (Array.isArray(layout)){\n var idx = 0;\n while (ret === null && idx < layout.length){\n ret = this.resolveScalableParameter(layout[idx], data);\n idx++;\n }\n } else {\n switch (typeof layout){\n case \"number\":\n case \"string\":\n ret = layout;\n break;\n case \"object\":\n if (layout.scale_function){\n if(layout.field) {\n var f = new LocusZoom.Data.Field(layout.field);\n ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, f.resolve(data));\n } else {\n ret = LocusZoom.ScaleFunctions.get(layout.scale_function, layout.parameters || {}, data);\n }\n }\n break;\n }\n }\n return ret;\n};\n\n/**\n * Generate dimension extent function based on layout parameters\n * @param {('x'|'y')} dimension\n */\nLocusZoom.DataLayer.prototype.getAxisExtent = function(dimension){\n\n if ([\"x\", \"y\"].indexOf(dimension) === -1){\n throw(\"Invalid dimension identifier passed to LocusZoom.DataLayer.getAxisExtent()\");\n }\n\n var axis_name = dimension + \"_axis\";\n var axis_layout = this.layout[axis_name];\n\n // If a floor AND a ceiling are explicitly defined then just return that extent and be done\n if (!isNaN(axis_layout.floor) && !isNaN(axis_layout.ceiling)){\n return [+axis_layout.floor, +axis_layout.ceiling];\n }\n\n // If a field is defined for the axis and the data layer has data then generate the extent from the data set\n var data_extent = [];\n if (axis_layout.field && this.data) {\n if (!this.data.length) {\n // If data has been fetched (but no points in region), enforce the min_extent (with no buffers,\n // because we don't need padding around an empty screen)\n data_extent = axis_layout.min_extent || [];\n return data_extent;\n } else {\n data_extent = d3.extent(this.data, function (d) {\n var f = new LocusZoom.Data.Field(axis_layout.field);\n return +f.resolve(d);\n });\n\n // Apply upper/lower buffers, if applicable\n var original_extent_span = data_extent[1] - data_extent[0];\n if (!isNaN(axis_layout.lower_buffer)) {\n data_extent[0] -= original_extent_span * axis_layout.lower_buffer;\n }\n if (!isNaN(axis_layout.upper_buffer)) {\n data_extent[1] += original_extent_span * axis_layout.upper_buffer;\n }\n\n if (typeof axis_layout.min_extent == \"object\") {\n // The data should span at least the range specified by min_extent, an array with [low, high]\n var range_min = axis_layout.min_extent[0];\n var range_max = axis_layout.min_extent[1];\n if (!isNaN(range_min) && !isNaN(range_max)) {\n data_extent[0] = Math.min(data_extent[0], range_min);\n }\n if (!isNaN(range_max)) {\n data_extent[1] = Math.max(data_extent[1], range_max);\n }\n }\n // If specified, floor and ceiling will override the actual data range\n return [\n isNaN(axis_layout.floor) ? data_extent[0] : axis_layout.floor,\n isNaN(axis_layout.ceiling) ? data_extent[1] : axis_layout.ceiling\n ];\n }\n }\n\n // If this is for the x axis and no extent could be generated yet but state has a defined start and end\n // then default to using the state-defined region as the extent\n if (dimension === \"x\" && !isNaN(this.state.start) && !isNaN(this.state.end)) {\n return [this.state.start, this.state.end];\n }\n\n // No conditions met for generating a valid extent, return an empty array\n return [];\n\n};\n\n/**\n * Allow this data layer to tell the panel what axis ticks it thinks it will require. The panel may choose whether\n * to use some, all, or none of these when rendering, either alone or in conjunction with other data layers.\n *\n * This method is a stub and should be overridden in data layers that need to specify custom behavior.\n *\n * @param {('x'|'y')} dimension\n * @param {Object} [config] Additional parameters for the panel to specify how it wants ticks to be drawn. The names\n * and meanings of these parameters may vary between different data layers.\n * @returns {Object[]}\n * An array of objects: each object must have an 'x' attribute to position the tick.\n * Other supported object keys:\n * * text: string to render for a given tick\n * * style: d3-compatible CSS style object\n * * transform: SVG transform attribute string\n * * color: string or LocusZoom scalable parameter object\n */\nLocusZoom.DataLayer.prototype.getTicks = function (dimension, config) {\n if ([\"x\", \"y\"].indexOf(dimension) === -1) {\n throw(\"Invalid dimension identifier\");\n }\n return [];\n};\n\n/**\n * Generate a tool tip for a given element\n * @param {String|Object} d The element associated with the tooltip\n * @param {String} [id] An identifier to the tooltip\n */\nLocusZoom.DataLayer.prototype.createTooltip = function(d, id){\n if (typeof this.layout.tooltip != \"object\"){\n throw (\"DataLayer [\" + this.id + \"] layout does not define a tooltip\");\n }\n if (typeof id == \"undefined\"){ id = this.getElementId(d); }\n if (this.tooltips[id]){\n this.positionTooltip(id);\n return;\n }\n this.tooltips[id] = {\n data: d,\n arrow: null,\n selector: d3.select(this.parent_plot.svg.node().parentNode).append(\"div\")\n .attr(\"class\", \"lz-data_layer-tooltip\")\n .attr(\"id\", id + \"-tooltip\")\n };\n this.updateTooltip(d);\n return this;\n};\n\n/**\n * Update a tool tip (generate its inner HTML)\n * @param {String|Object} d The element associated with the tooltip\n * @param {String} [id] An identifier to the tooltip\n */\nLocusZoom.DataLayer.prototype.updateTooltip = function(d, id){\n if (typeof id == \"undefined\"){ id = this.getElementId(d); }\n // Empty the tooltip of all HTML (including its arrow!)\n this.tooltips[id].selector.html(\"\");\n this.tooltips[id].arrow = null;\n // Set the new HTML\n if (this.layout.tooltip.html){\n this.tooltips[id].selector.html(LocusZoom.parseFields(d, this.layout.tooltip.html));\n }\n // If the layout allows tool tips on this data layer to be closable then add the close button\n // and add padding to the tooltip to accommodate it\n if (this.layout.tooltip.closable){\n this.tooltips[id].selector.insert(\"button\", \":first-child\")\n .attr(\"class\", \"lz-tooltip-close-button\")\n .attr(\"title\", \"Close\")\n .text(\"×\")\n .on(\"click\", function(){\n this.destroyTooltip(id);\n }.bind(this));\n }\n // Apply data directly to the tool tip for easier retrieval by custom UI elements inside the tool tip\n this.tooltips[id].selector.data([d]);\n // Reposition and draw a new arrow\n this.positionTooltip(id);\n return this;\n};\n\n/**\n * Destroy tool tip - remove the tool tip element from the DOM and delete the tool tip's record on the data layer\n * @param {String|Object} d The element associated with the tooltip\n * @param {String} [id] An identifier to the tooltip\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.destroyTooltip = function(d, id){\n if (typeof d == \"string\"){\n id = d;\n } else if (typeof id == \"undefined\"){\n id = this.getElementId(d);\n }\n if (this.tooltips[id]){\n if (typeof this.tooltips[id].selector == \"object\"){\n this.tooltips[id].selector.remove();\n }\n delete this.tooltips[id];\n }\n return this;\n};\n\n/**\n * Loop through and destroy all tool tips on this data layer\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.destroyAllTooltips = function(){\n for (var id in this.tooltips){\n this.destroyTooltip(id);\n }\n return this;\n};\n\n//\n/**\n * Position tool tip - naïve function to place a tool tip to the lower right of the current mouse element\n * Most data layers reimplement this method to position tool tips specifically for the data they display\n * @param {String} id The identifier of the tooltip to position\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n // Position the div itself\n this.tooltips[id].selector\n .style(\"left\", (d3.event.pageX) + \"px\")\n .style(\"top\", (d3.event.pageY) + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!this.tooltips[id].arrow){\n this.tooltips[id].arrow = this.tooltips[id].selector.append(\"div\")\n .style(\"position\", \"absolute\")\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_top_left\");\n }\n this.tooltips[id].arrow\n .style(\"left\", \"-1px\")\n .style(\"top\", \"-1px\");\n return this;\n};\n\n/**\n * Loop through and position all tool tips on this data layer\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.positionAllTooltips = function(){\n for (var id in this.tooltips){\n this.positionTooltip(id);\n }\n return this;\n};\n\n/**\n * Show or hide a tool tip by ID depending on directives in the layout and state values relative to the ID\n * @param {String|Object} element The element associated with the tooltip\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.showOrHideTooltip = function(element){\n \n if (typeof this.layout.tooltip != \"object\"){ return; }\n var id = this.getElementId(element);\n\n var resolveStatus = function(statuses, directive, operator){\n var status = null;\n if (typeof statuses != \"object\" || statuses === null){ return null; }\n if (Array.isArray(directive)){\n if (typeof operator == \"undefined\"){ operator = \"and\"; }\n if (directive.length === 1){\n status = statuses[directive[0]];\n } else {\n status = directive.reduce(function(previousValue, currentValue) {\n if (operator === \"and\"){\n return statuses[previousValue] && statuses[currentValue];\n } else if (operator === \"or\"){\n return statuses[previousValue] || statuses[currentValue];\n }\n return null;\n });\n }\n } else if (typeof directive == \"object\"){\n var sub_status;\n for (var sub_operator in directive){\n sub_status = resolveStatus(statuses, directive[sub_operator], sub_operator);\n if (status === null){\n status = sub_status;\n } else if (operator === \"and\"){\n status = status && sub_status;\n } else if (operator === \"or\"){\n status = status || sub_status;\n }\n }\n }\n return status;\n };\n\n var show_directive = {};\n if (typeof this.layout.tooltip.show == \"string\"){\n show_directive = { and: [ this.layout.tooltip.show ] };\n } else if (typeof this.layout.tooltip.show == \"object\"){\n show_directive = this.layout.tooltip.show;\n }\n\n var hide_directive = {};\n if (typeof this.layout.tooltip.hide == \"string\"){\n hide_directive = { and: [ this.layout.tooltip.hide ] };\n } else if (typeof this.layout.tooltip.hide == \"object\"){\n hide_directive = this.layout.tooltip.hide;\n }\n\n var statuses = {};\n LocusZoom.DataLayer.Statuses.adjectives.forEach(function(status){\n var antistatus = \"un\" + status;\n statuses[status] = this.state[this.state_id][status].indexOf(id) !== -1;\n statuses[antistatus] = !statuses[status];\n }.bind(this));\n\n var show_resolved = resolveStatus(statuses, show_directive);\n var hide_resolved = resolveStatus(statuses, hide_directive);\n\n // Only show tooltip if the resolved logic explicitly shows and explicitly not hides the tool tip\n // Otherwise ensure tooltip does not exist\n if (show_resolved && !hide_resolved){\n this.createTooltip(element);\n } else {\n this.destroyTooltip(element);\n }\n\n return this;\n \n};\n\n/**\n * Find the elements (or indices) that match any of a set of provided filters\n * @protected\n * @param {Array[]} filters A list of filter entries: [field, value] (for equivalence testing) or\n * [field, operator, value] for other operators\n * @param {('indexes'|'elements')} [return_type='indexes'] Specify whether to return either the indices of the matching\n * elements, or references to the elements themselves\n * @returns {Array}\n */\nLocusZoom.DataLayer.prototype.filter = function(filters, return_type){\n if (typeof return_type == \"undefined\" || [\"indexes\",\"elements\"].indexOf(return_type) === -1){\n return_type = \"indexes\";\n }\n if (!Array.isArray(filters)){ return []; }\n var test = function(element, filter){\n var operators = {\n \"=\": function(a,b){ return a === b; },\n \"<\": function(a,b){ return a < b; },\n \"<=\": function(a,b){ return a <= b; },\n \">\": function(a,b){ return a > b; },\n \">=\": function(a,b){ return a >= b; },\n \"%\": function(a,b){ return a % b; }\n };\n if (!Array.isArray(filter)){ return false; }\n if (filter.length === 2){\n return element[filter[0]] === filter[1];\n } else if (filter.length === 3 && operators[filter[1]]){\n return operators[filter[1]](element[filter[0]], filter[2]);\n } else {\n return false;\n }\n };\n var matches = [];\n this.data.forEach(function(element, idx){\n var match = true;\n filters.forEach(function(filter){\n if (!test(element, filter)){ match = false; }\n });\n if (match){ matches.push(return_type === \"indexes\" ? idx : element); }\n });\n return matches;\n};\n\n/**\n * @param filters\n * @returns {Array}\n */\nLocusZoom.DataLayer.prototype.filterIndexes = function(filters){ return this.filter(filters, \"indexes\"); };\n/**\n * @param filters\n * @returns {Array}\n */\nLocusZoom.DataLayer.prototype.filterElements = function(filters){ return this.filter(filters, \"elements\"); };\n\nLocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){\n var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx];\n var antiverb = \"un\" + verb;\n // Set/unset a single element's status\n // TODO: Improve documentation for dynamically generated methods/properties\n LocusZoom.DataLayer.prototype[verb + \"Element\"] = function(element, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n this.setElementStatus(adjective, element, true, exclusive);\n return this;\n };\n LocusZoom.DataLayer.prototype[antiverb + \"Element\"] = function(element, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n this.setElementStatus(adjective, element, false, exclusive);\n return this;\n };\n // Set/unset status for arbitrarily many elements given a set of filters\n LocusZoom.DataLayer.prototype[verb + \"ElementsByFilters\"] = function(filters, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n return this.setElementStatusByFilters(adjective, true, filters, exclusive);\n };\n LocusZoom.DataLayer.prototype[antiverb + \"ElementsByFilters\"] = function(filters, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n return this.setElementStatusByFilters(adjective, false, filters, exclusive);\n };\n // Set/unset status for all elements\n LocusZoom.DataLayer.prototype[verb + \"AllElements\"] = function(){\n this.setAllElementStatus(adjective, true);\n return this;\n };\n LocusZoom.DataLayer.prototype[antiverb + \"AllElements\"] = function(){\n this.setAllElementStatus(adjective, false);\n return this;\n };\n});\n\n/**\n * Toggle a status (e.g. highlighted, selected, identified) on an element\n * @param {String} status\n * @param {String|Object} element\n * @param {Boolean} toggle\n * @param {Boolean} exclusive\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.setElementStatus = function(status, element, toggle, exclusive){\n \n // Sanity checks\n if (typeof status == \"undefined\" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){\n throw(\"Invalid status passed to DataLayer.setElementStatus()\");\n }\n if (typeof element == \"undefined\"){\n throw(\"Invalid element passed to DataLayer.setElementStatus()\");\n }\n if (typeof toggle == \"undefined\"){\n toggle = true;\n }\n\n // Get an ID for the element or return having changed nothing\n try {\n var element_id = this.getElementId(element);\n } catch (get_element_id_error){\n return this;\n }\n\n // Enforce exclusivity (force all elements to have the opposite of toggle first)\n if (exclusive){\n this.setAllElementStatus(status, !toggle);\n }\n \n // Set/unset the proper status class on the appropriate DOM element(s)\n d3.select(\"#\" + element_id).classed(\"lz-data_layer-\" + this.layout.type + \"-\" + status, toggle);\n var element_status_node_id = this.getElementStatusNodeId(element);\n if (element_status_node_id !== null){\n d3.select(\"#\" + element_status_node_id).classed(\"lz-data_layer-\" + this.layout.type + \"-statusnode-\" + status, toggle);\n }\n \n // Track element ID in the proper status state array\n var element_status_idx = this.state[this.state_id][status].indexOf(element_id);\n if (toggle && element_status_idx === -1){\n this.state[this.state_id][status].push(element_id);\n }\n if (!toggle && element_status_idx !== -1){\n this.state[this.state_id][status].splice(element_status_idx, 1);\n }\n \n // Trigger tool tip show/hide logic\n this.showOrHideTooltip(element);\n\n // Trigger layout changed event hook\n this.parent.emit(\"layout_changed\");\n this.parent_plot.emit(\"layout_changed\");\n\n return this;\n \n};\n\n/**\n * Toggle a status on elements in the data layer based on a set of filters\n * @param {String} status\n * @param {Boolean} toggle\n * @param {Array} filters\n * @param {Boolean} exclusive\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.setElementStatusByFilters = function(status, toggle, filters, exclusive){\n \n // Sanity check\n if (typeof status == \"undefined\" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){\n throw(\"Invalid status passed to DataLayer.setElementStatusByFilters()\");\n }\n if (typeof this.state[this.state_id][status] == \"undefined\"){ return this; }\n if (typeof toggle == \"undefined\"){ toggle = true; } else { toggle = !!toggle; }\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n if (!Array.isArray(filters)){ filters = []; }\n\n // Enforce exclusivity (force all elements to have the opposite of toggle first)\n if (exclusive){\n this.setAllElementStatus(status, !toggle);\n }\n \n // Apply statuses\n this.filterElements(filters).forEach(function(element){\n this.setElementStatus(status, element, toggle);\n }.bind(this));\n \n return this;\n};\n\n/**\n * Toggle a status on all elements in the data layer\n * @param {String} status\n * @param {Boolean} toggle\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.setAllElementStatus = function(status, toggle){\n \n // Sanity check\n if (typeof status == \"undefined\" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){\n throw(\"Invalid status passed to DataLayer.setAllElementStatus()\");\n }\n if (typeof this.state[this.state_id][status] == \"undefined\"){ return this; }\n if (typeof toggle == \"undefined\"){ toggle = true; }\n\n // Apply statuses\n if (toggle){\n this.data.forEach(function(element){\n this.setElementStatus(status, element, true);\n }.bind(this));\n } else {\n var status_ids = this.state[this.state_id][status].slice();\n status_ids.forEach(function(id){\n var element = this.getElementById(id);\n if (typeof element == \"object\" && element !== null){\n this.setElementStatus(status, element, false);\n }\n }.bind(this));\n this.state[this.state_id][status] = [];\n }\n\n // Update global status flag\n this.global_statuses[status] = toggle;\n\n return this;\n};\n\n/**\n * Apply all layout-defined behaviors to a selection of elements with event handlers\n * @param {d3.selection} selection\n */\nLocusZoom.DataLayer.prototype.applyBehaviors = function(selection){\n if (typeof this.layout.behaviors != \"object\"){ return; }\n Object.keys(this.layout.behaviors).forEach(function(directive){\n var event_match = /(click|mouseover|mouseout)/.exec(directive);\n if (!event_match){ return; }\n selection.on(event_match[0] + \".\" + directive, this.executeBehaviors(directive, this.layout.behaviors[directive]));\n }.bind(this));\n};\n\n/**\n * Generate a function that executes an arbitrary list of behaviors on an element during an event\n * @param {String} directive The name of the event, as described in layout.behaviors for this datalayer\n * @param {Object} behaviors An object describing the behavior to attach to this single element\n * @param {string} behaviors.action The name of the action that would trigger this behavior (eg click, mouseover, etc)\n * @param {string} behaviors.status What status to apply to the element when this behavior is triggered (highlighted,\n * selected, etc)\n * @param {string} [behaviors.exclusive] Whether triggering the event for this element should unset the relevant status\n * for all other elements. Useful for, eg, click events that exclusively highlight one thing.\n * @returns {function(this:LocusZoom.DataLayer)} Return a function that handles the event in context with the behavior\n * and the element- can be attached as an event listener\n */\nLocusZoom.DataLayer.prototype.executeBehaviors = function(directive, behaviors) {\n\n // Determine the required state of control and shift keys during the event\n var requiredKeyStates = {\n \"ctrl\": (directive.indexOf(\"ctrl\") !== -1),\n \"shift\": (directive.indexOf(\"shift\") !== -1)\n };\n\n return function(element){\n\n // Do nothing if the required control and shift key presses (or lack thereof) doesn't match the event\n if (requiredKeyStates.ctrl !== !!d3.event.ctrlKey || requiredKeyStates.shift !== !!d3.event.shiftKey){ return; }\n\n // Loop through behaviors making each one go in succession\n behaviors.forEach(function(behavior){\n \n // Route first by the action, if defined\n if (typeof behavior != \"object\" || behavior === null){ return; }\n \n switch (behavior.action){\n \n // Set a status (set to true regardless of current status, optionally with exclusivity)\n case \"set\":\n this.setElementStatus(behavior.status, element, true, behavior.exclusive);\n break;\n \n // Unset a status (set to false regardless of current status, optionally with exclusivity)\n case \"unset\":\n this.setElementStatus(behavior.status, element, false, behavior.exclusive);\n break;\n \n // Toggle a status\n case \"toggle\":\n var current_status_boolean = (this.state[this.state_id][behavior.status].indexOf(this.getElementId(element)) !== -1);\n var exclusive = behavior.exclusive && !current_status_boolean;\n this.setElementStatus(behavior.status, element, !current_status_boolean, exclusive);\n break;\n \n // Link to a dynamic URL\n case \"link\":\n if (typeof behavior.href == \"string\"){\n var url = LocusZoom.parseFields(element, behavior.href);\n if (typeof behavior.target == \"string\"){\n window.open(url, behavior.target);\n } else {\n window.location.href = url;\n }\n }\n break;\n \n // Action not defined, just return\n default:\n break;\n \n }\n \n return;\n \n }.bind(this));\n\n }.bind(this);\n\n};\n\n/**\n * Get an object with the x and y coordinates of the panel's origin in terms of the entire page\n * Necessary for positioning any HTML elements over the panel\n * @returns {{x: Number, y: Number}}\n */\nLocusZoom.DataLayer.prototype.getPageOrigin = function(){\n var panel_origin = this.parent.getPageOrigin();\n return {\n x: panel_origin.x + this.parent.layout.margin.left,\n y: panel_origin.y + this.parent.layout.margin.top\n };\n};\n\n/**\n * Get a data layer's current underlying data in a standard format (e.g. JSON or CSV)\n * @param {('csv'|'tsv'|'json')} format How to export the data\n * @returns {*}\n */\nLocusZoom.DataLayer.prototype.exportData = function(format){\n var default_format = \"json\";\n format = format || default_format;\n format = (typeof format == \"string\" ? format.toLowerCase() : default_format);\n if ([\"json\",\"csv\",\"tsv\"].indexOf(format) === -1){ format = default_format; }\n var ret;\n switch (format){\n case \"json\":\n try {\n ret = JSON.stringify(this.data);\n } catch (e){\n ret = null;\n console.error(\"Unable to export JSON data from data layer: \" + this.getBaseId() + \";\", e);\n }\n break;\n case \"tsv\":\n case \"csv\":\n try {\n var jsonified = JSON.parse(JSON.stringify(this.data));\n if (typeof jsonified != \"object\"){\n ret = jsonified.toString();\n } else if (!Array.isArray(jsonified)){\n ret = \"Object\";\n } else {\n var delimiter = (format === \"tsv\") ? \"\\t\" : \",\";\n var header = this.layout.fields.map(function(header){\n return JSON.stringify(header);\n }).join(delimiter) + \"\\n\";\n ret = header + jsonified.map(function(record){\n return this.layout.fields.map(function(field){\n if (typeof record[field] == \"undefined\"){\n return JSON.stringify(null);\n } else if (typeof record[field] == \"object\" && record[field] !== null){\n return Array.isArray(record[field]) ? \"\\\"[Array(\" + record[field].length + \")]\\\"\" : \"\\\"[Object]\\\"\";\n } else {\n return JSON.stringify(record[field]);\n }\n }).join(delimiter);\n }.bind(this)).join(\"\\n\");\n }\n } catch (e){\n ret = null;\n console.error(\"Unable to export CSV data from data layer: \" + this.getBaseId() + \";\", e);\n }\n break;\n }\n return ret;\n};\n\n/**\n * Position the datalayer and all tooltips\n * @returns {LocusZoom.DataLayer}\n */\nLocusZoom.DataLayer.prototype.draw = function(){\n this.svg.container.attr(\"transform\", \"translate(\" + this.parent.layout.cliparea.origin.x + \",\" + this.parent.layout.cliparea.origin.y + \")\");\n this.svg.clipRect\n .attr(\"width\", this.parent.layout.cliparea.width)\n .attr(\"height\", this.parent.layout.cliparea.height);\n this.positionAllTooltips();\n return this;\n};\n\n\n/**\n * Re-Map a data layer to reflect changes in the state of a plot (such as viewing region/ chromosome range)\n * @return {Promise}\n */\nLocusZoom.DataLayer.prototype.reMap = function(){\n\n this.destroyAllTooltips(); // hack - only non-visible tooltips should be destroyed\n // and then recreated if returning to visibility\n\n // Fetch new data\n var promise = this.parent_plot.lzd.getData(this.state, this.layout.fields); //,\"ld:best\"\n promise.then(function(new_data){\n this.data = new_data.body;\n this.applyDataMethods();\n this.initialized = true;\n }.bind(this));\n\n return promise;\n\n};\n\n\n/**\n * The central registry of known data layer definitions (which may be stored in separate files due to length)\n * @namespace\n */\nLocusZoom.DataLayers = (function() {\n var obj = {};\n var datalayers = {};\n /**\n * @name LocusZoom.DataLayers.get\n * @param {String} name The name of the datalayer\n * @param {Object} layout The configuration object for this data layer\n * @param {LocusZoom.DataLayer|LocusZoom.Panel} parent Where this layout is used\n * @returns {LocusZoom.DataLayer}\n */\n obj.get = function(name, layout, parent) {\n if (!name) {\n return null;\n } else if (datalayers[name]) {\n if (typeof layout != \"object\"){\n throw(\"invalid layout argument for data layer [\" + name + \"]\");\n } else {\n return new datalayers[name](layout, parent);\n }\n } else {\n throw(\"data layer [\" + name + \"] not found\");\n }\n };\n\n /**\n * @name LocusZoom.DataLayers.set\n * @protected\n * @param {String} name\n * @param {Function} datalayer Constructor for the datalayer\n */\n obj.set = function(name, datalayer) {\n if (datalayer) {\n if (typeof datalayer != \"function\"){\n throw(\"unable to set data layer [\" + name + \"], argument provided is not a function\");\n } else {\n datalayers[name] = datalayer;\n datalayers[name].prototype = new LocusZoom.DataLayer();\n }\n } else {\n delete datalayers[name];\n }\n };\n\n /**\n * Add a new type of datalayer to the registry of known layer types\n * @name LocusZoom.DataLayers.add\n * @param {String} name The name of the data layer to register\n * @param {Function} datalayer\n */\n obj.add = function(name, datalayer) {\n if (datalayers[name]) {\n throw(\"data layer already exists with name: \" + name);\n } else {\n obj.set(name, datalayer);\n }\n };\n\n /**\n * Register a new datalayer that inherits and extends basic behaviors from a known datalayer\n * @param {String} parent_name The name of the parent data layer whose behavior is to be extended\n * @param {String} name The name of the new datalayer to register\n * @param {Object} [overrides] Object of properties and methods to combine with the prototype of the parent datalayer\n * @returns {Function} The constructor for the new child class\n */\n obj.extend = function(parent_name, name, overrides) {\n // TODO: Consider exposing additional constructor argument, if there is a use case for very granular extension\n overrides = overrides || {};\n\n var parent = datalayers[parent_name];\n if (!parent) {\n throw \"Attempted to subclass an unknown or unregistered datalayer type\";\n }\n if (typeof overrides !== \"object\") {\n throw \"Must specify an object of properties and methods\";\n }\n var child = LocusZoom.subclass(parent, overrides);\n // Bypass .set() because we want a layer of inheritance below `DataLayer`\n datalayers[name] = child;\n return child;\n };\n\n /**\n * List the names of all known datalayers\n * @name LocusZoom.DataLayers.list\n * @returns {String[]}\n */\n obj.list = function() {\n return Object.keys(datalayers);\n };\n\n return obj;\n})();\n","\"use strict\";\n\n/**\n * Create a single continuous 2D track that provides information about each datapoint\n *\n * For example, this can be used to color by membership in a group, alongside information in other panels\n *\n * @class LocusZoom.DataLayers.annotation_track\n * @augments LocusZoom.DataLayer\n * @param {Object} layout\n * @param {Object|String} [layout.color]\n * @param {Array[]} An array of filter entries specifying which points to draw annotations for.\n * See `LocusZoom.DataLayer.filter` for details\n */\nLocusZoom.DataLayers.add(\"annotation_track\", function(layout) {\n // In the future we may add additional options for controlling marker size/ shape, based on user feedback\n this.DefaultLayout = {\n color: \"#000000\",\n filters: []\n };\n\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n if (!Array.isArray(layout.filters)) {\n throw \"Annotation track must specify array of filters for selecting points to annotate\";\n }\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n this.render = function() {\n var self = this;\n // Only render points that currently satisfy all provided filter conditions.\n var trackData = this.filter(this.layout.filters, \"elements\");\n\n var selection = this.svg.group\n .selectAll(\"rect.lz-data_layer-\" + self.layout.type)\n .data(trackData, function(d) { return d[self.layout.id_field]; });\n\n // Add new elements as needed\n selection.enter()\n .append(\"rect\")\n .attr(\"class\", \"lz-data_layer-\" + this.layout.type)\n .attr(\"id\", function (d){ return self.getElementId(d); });\n // Update the set of elements to reflect new data\n selection\n .attr(\"x\", function (d) { return self.parent[\"x_scale\"](d[self.layout.x_axis.field]); })\n .attr(\"width\", 1) // TODO autocalc width of track? Based on datarange / pixel width presumably\n .attr(\"height\", self.parent.layout.height)\n .attr(\"fill\", function(d){ return self.resolveScalableParameter(self.layout.color, d); });\n // Remove unused elements\n selection.exit().remove();\n\n // Set up tooltips and mouse interaction\n this.applyBehaviors(selection);\n };\n\n // Reimplement the positionTooltip() method to be annotation-specific\n this.positionTooltip = function(id) {\n if (typeof id != \"string\") {\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]) {\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var top, left, arrow_type, arrow_top, arrow_left;\n var tooltip = this.tooltips[id];\n var arrow_width = 7; // as defined in the default stylesheet\n var stroke_width = 1; // as defined in the default stylesheet\n var offset = stroke_width / 2;\n var page_origin = this.getPageOrigin();\n\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right);\n\n var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]);\n var y_center = data_layer_height / 2;\n\n // Tooltip should be horizontally centered above the point to be annotated. (or below if space is limited)\n var offset_right = Math.max((tooltip_box.width / 2) - x_center, 0);\n var offset_left = Math.max((tooltip_box.width / 2) + x_center - data_layer_width, 0);\n left = page_origin.x + x_center - (tooltip_box.width / 2) - offset_left + offset_right;\n arrow_left = (tooltip_box.width / 2) - (arrow_width) + offset_left - offset_right - offset;\n if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - y_center) {\n top = page_origin.y + y_center - (tooltip_box.height + stroke_width + arrow_width);\n arrow_type = \"down\";\n arrow_top = tooltip_box.height - stroke_width;\n } else {\n top = page_origin.y + y_center + stroke_width + arrow_width;\n arrow_type = \"up\";\n arrow_top = 0 - stroke_width - arrow_width;\n }\n // Apply positions to the main div\n tooltip.selector.style(\"left\", left + \"px\").style(\"top\", top + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow) {\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style(\"left\", arrow_left + \"px\")\n .style(\"top\", arrow_top + \"px\");\n };\n\n return this;\n});\n","\"use strict\";\n\n/*********************\n Forest Data Layer\n Implements a standard forest plot\n*/\n\nLocusZoom.DataLayers.add(\"forest\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n this.DefaultLayout = {\n point_size: 40,\n point_shape: \"square\",\n color: \"#888888\",\n fill_opacity: 1,\n y_axis: {\n axis: 2\n },\n id_field: \"id\",\n confidence_intervals: {\n start_field: \"ci_start\",\n end_field: \"ci_end\"\n },\n show_no_significance_line: true\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n // Reimplement the positionTooltip() method to be forest-specific\n this.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]){\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var tooltip = this.tooltips[id];\n var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data);\n var arrow_width = 7; // as defined in the default stylesheet\n var stroke_width = 1; // as defined in the default stylesheet\n var border_radius = 6; // as defined in the default stylesheet\n var page_origin = this.getPageOrigin();\n var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]);\n var y_scale = \"y\"+this.layout.y_axis.axis+\"_scale\";\n var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]);\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n // Position horizontally on the left or the right depending on which side of the plot the point is on\n var offset = Math.sqrt(point_size / Math.PI);\n var left, arrow_type, arrow_left;\n if (x_center <= this.parent.layout.width / 2){\n left = page_origin.x + x_center + offset + arrow_width + stroke_width;\n arrow_type = \"left\";\n arrow_left = -1 * (arrow_width + stroke_width);\n } else {\n left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width;\n arrow_type = \"right\";\n arrow_left = tooltip_box.width - stroke_width;\n }\n // Position vertically centered unless we're at the top or bottom of the plot\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var top, arrow_top;\n if (y_center - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down\n top = page_origin.y + y_center - (1.5 * arrow_width) - border_radius;\n arrow_top = border_radius;\n } else if (y_center + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up\n top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height;\n arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius;\n } else { // vertically centered\n top = page_origin.y + y_center - (tooltip_box.height / 2);\n arrow_top = (tooltip_box.height / 2) - arrow_width;\n } \n // Apply positions to the main div\n tooltip.selector.style(\"left\", left + \"px\").style(\"top\", top + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow){\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style(\"left\", arrow_left + \"px\")\n .style(\"top\", arrow_top + \"px\");\n };\n\n // Implement the main render function\n this.render = function(){\n\n var x_scale = \"x_scale\";\n var y_scale = \"y\"+this.layout.y_axis.axis+\"_scale\";\n\n // Generate confidence interval paths if fields are defined\n if (this.layout.confidence_intervals\n && this.layout.fields.indexOf(this.layout.confidence_intervals.start_field) !== -1\n && this.layout.fields.indexOf(this.layout.confidence_intervals.end_field) !== -1){\n // Generate a selection for all forest plot confidence intervals\n var ci_selection = this.svg.group\n .selectAll(\"rect.lz-data_layer-forest.lz-data_layer-forest-ci\")\n .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this));\n // Create confidence interval rect elements\n ci_selection.enter()\n .append(\"rect\")\n .attr(\"class\", \"lz-data_layer-forest lz-data_layer-forest-ci\")\n .attr(\"id\", function(d){ return this.getElementId(d) + \"_ci\"; }.bind(this))\n .attr(\"transform\", \"translate(0,\" + (isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height) + \")\");\n // Apply position and size parameters using transition if necessary\n var ci_transform = function(d) {\n var x = this.parent[x_scale](d[this.layout.confidence_intervals.start_field]);\n var y = this.parent[y_scale](d[this.layout.y_axis.field]);\n if (isNaN(x)){ x = -1000; }\n if (isNaN(y)){ y = -1000; }\n return \"translate(\" + x + \",\" + y + \")\";\n }.bind(this);\n var ci_width = function(d){\n return this.parent[x_scale](d[this.layout.confidence_intervals.end_field])\n - this.parent[x_scale](d[this.layout.confidence_intervals.start_field]);\n }.bind(this);\n var ci_height = 1;\n if (this.canTransition()){\n ci_selection\n .transition()\n .duration(this.layout.transition.duration || 0)\n .ease(this.layout.transition.ease || \"cubic-in-out\")\n .attr(\"transform\", ci_transform)\n .attr(\"width\", ci_width).attr(\"height\", ci_height);\n } else {\n ci_selection\n .attr(\"transform\", ci_transform)\n .attr(\"width\", ci_width).attr(\"height\", ci_height);\n }\n // Remove old elements as needed\n ci_selection.exit().remove();\n }\n \n // Generate a selection for all forest plot points\n var points_selection = this.svg.group\n .selectAll(\"path.lz-data_layer-forest.lz-data_layer-forest-point\")\n .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this));\n\n // Create elements, apply class, ID, and initial position\n var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height;\n points_selection.enter()\n .append(\"path\")\n .attr(\"class\", \"lz-data_layer-forest lz-data_layer-forest-point\")\n .attr(\"id\", function(d){ return this.getElementId(d) + \"_point\"; }.bind(this))\n .attr(\"transform\", \"translate(0,\" + initial_y + \")\");\n\n // Generate new values (or functions for them) for position, color, size, and shape\n var transform = function(d) {\n var x = this.parent[x_scale](d[this.layout.x_axis.field]);\n var y = this.parent[y_scale](d[this.layout.y_axis.field]);\n if (isNaN(x)){ x = -1000; }\n if (isNaN(y)){ y = -1000; }\n return \"translate(\" + x + \",\" + y + \")\";\n }.bind(this);\n\n var fill = function(d){ return this.resolveScalableParameter(this.layout.color, d); }.bind(this);\n var fill_opacity = function(d){ return this.resolveScalableParameter(this.layout.fill_opacity, d); }.bind(this);\n\n var shape = d3.svg.symbol()\n .size(function(d){ return this.resolveScalableParameter(this.layout.point_size, d); }.bind(this))\n .type(function(d){ return this.resolveScalableParameter(this.layout.point_shape, d); }.bind(this));\n\n // Apply position and color, using a transition if necessary\n if (this.canTransition()){\n points_selection\n .transition()\n .duration(this.layout.transition.duration || 0)\n .ease(this.layout.transition.ease || \"cubic-in-out\")\n .attr(\"transform\", transform)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity)\n .attr(\"d\", shape);\n } else {\n points_selection\n .attr(\"transform\", transform)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity)\n .attr(\"d\", shape);\n }\n\n // Remove old elements as needed\n points_selection.exit().remove();\n\n // Apply default event emitters to selection\n points_selection.on(\"click.event_emitter\", function(element){\n this.parent.emit(\"element_clicked\", element);\n this.parent_plot.emit(\"element_clicked\", element);\n }.bind(this));\n \n // Apply behaviors to points\n this.applyBehaviors(points_selection);\n \n };\n \n return this;\n\n});\n","\"use strict\";\n\n/*********************\n * Genes Data Layer\n * Implements a data layer that will render gene tracks\n * @class\n * @augments LocusZoom.DataLayer\n*/\nLocusZoom.DataLayers.add(\"genes\", function(layout){\n /**\n * Define a default layout for this DataLayer type and merge it with the passed argument\n * @protected\n * @member {Object}\n * */\n this.DefaultLayout = {\n label_font_size: 12,\n label_exon_spacing: 4,\n exon_height: 16,\n bounding_box_padding: 6,\n track_vertical_spacing: 10,\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n /**\n * Generate a statusnode ID for a given element\n * @override\n * @returns {String}\n */\n this.getElementStatusNodeId = function(element){\n return this.getElementId(element) + \"-statusnode\";\n };\n\n /**\n * Helper function to sum layout values to derive total height for a single gene track\n * @returns {number}\n */\n this.getTrackHeight = function(){\n return 2 * this.layout.bounding_box_padding\n + this.layout.label_font_size\n + this.layout.label_exon_spacing\n + this.layout.exon_height\n + this.layout.track_vertical_spacing;\n };\n\n /**\n * A gene may have arbitrarily many transcripts, but this data layer isn't set up to render them yet.\n * Stash a transcript_idx to point to the first transcript and use that for all transcript refs.\n * @member {number}\n * @type {number}\n */\n this.transcript_idx = 0;\n\n /**\n * An internal counter for the number of tracks in the data layer. Used as an internal counter for looping\n * over positions / assignments\n * @protected\n * @member {number}\n */\n this.tracks = 1;\n\n /**\n * Store information about genes in dataset, in a hash indexed by track number: {track_number: [gene_indices]}\n * @member {Object.}\n */\n this.gene_track_index = { 1: [] };\n\n /**\n * Ensure that genes in overlapping chromosome regions are positioned so that parts of different genes do not\n * overlap in the view. A track is a row used to vertically separate overlapping genes.\n * @returns {LocusZoom.DataLayer}\n */\n this.assignTracks = function(){\n /**\n * Function to get the width in pixels of a label given the text and layout attributes\n * TODO: Move to outer scope?\n * @param {String} gene_name\n * @param {number|string} font_size\n * @returns {number}\n */\n this.getLabelWidth = function(gene_name, font_size){\n try {\n var temp_text = this.svg.group.append(\"text\")\n .attr(\"x\", 0).attr(\"y\", 0).attr(\"class\", \"lz-data_layer-genes lz-label\")\n .style(\"font-size\", font_size)\n .text(gene_name + \"→\");\n var label_width = temp_text.node().getBBox().width;\n temp_text.remove();\n return label_width;\n } catch (e){\n return 0;\n }\n };\n\n // Reinitialize some metadata\n this.tracks = 1;\n this.gene_track_index = { 1: [] };\n\n this.data.map(function(d, g){\n\n // If necessary, split combined gene id / version fields into discrete fields.\n // NOTE: this may be an issue with CSG's genes data source that may eventually be solved upstream.\n if (this.data[g].gene_id && this.data[g].gene_id.indexOf(\".\")){\n var split = this.data[g].gene_id.split(\".\");\n this.data[g].gene_id = split[0];\n this.data[g].gene_version = split[1];\n }\n\n // Stash the transcript ID on the parent gene\n this.data[g].transcript_id = this.data[g].transcripts[this.transcript_idx].transcript_id;\n\n // Determine display range start and end, based on minimum allowable gene display width, bounded by what we can see\n // (range: values in terms of pixels on the screen)\n this.data[g].display_range = {\n start: this.parent.x_scale(Math.max(d.start, this.state.start)),\n end: this.parent.x_scale(Math.min(d.end, this.state.end))\n };\n this.data[g].display_range.label_width = this.getLabelWidth(this.data[g].gene_name, this.layout.label_font_size);\n this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start;\n // Determine label text anchor (default to middle)\n this.data[g].display_range.text_anchor = \"middle\";\n if (this.data[g].display_range.width < this.data[g].display_range.label_width){\n if (d.start < this.state.start){\n this.data[g].display_range.end = this.data[g].display_range.start\n + this.data[g].display_range.label_width\n + this.layout.label_font_size;\n this.data[g].display_range.text_anchor = \"start\";\n } else if (d.end > this.state.end){\n this.data[g].display_range.start = this.data[g].display_range.end\n - this.data[g].display_range.label_width\n - this.layout.label_font_size;\n this.data[g].display_range.text_anchor = \"end\";\n } else {\n var centered_margin = ((this.data[g].display_range.label_width - this.data[g].display_range.width) / 2)\n + this.layout.label_font_size;\n if ((this.data[g].display_range.start - centered_margin) < this.parent.x_scale(this.state.start)){\n this.data[g].display_range.start = this.parent.x_scale(this.state.start);\n this.data[g].display_range.end = this.data[g].display_range.start + this.data[g].display_range.label_width;\n this.data[g].display_range.text_anchor = \"start\";\n } else if ((this.data[g].display_range.end + centered_margin) > this.parent.x_scale(this.state.end)) {\n this.data[g].display_range.end = this.parent.x_scale(this.state.end);\n this.data[g].display_range.start = this.data[g].display_range.end - this.data[g].display_range.label_width;\n this.data[g].display_range.text_anchor = \"end\";\n } else {\n this.data[g].display_range.start -= centered_margin;\n this.data[g].display_range.end += centered_margin;\n }\n }\n this.data[g].display_range.width = this.data[g].display_range.end - this.data[g].display_range.start;\n }\n // Add bounding box padding to the calculated display range start, end, and width\n this.data[g].display_range.start -= this.layout.bounding_box_padding;\n this.data[g].display_range.end += this.layout.bounding_box_padding;\n this.data[g].display_range.width += 2 * this.layout.bounding_box_padding;\n // Convert and stash display range values into domain values\n // (domain: values in terms of the data set, e.g. megabases)\n this.data[g].display_domain = {\n start: this.parent.x_scale.invert(this.data[g].display_range.start),\n end: this.parent.x_scale.invert(this.data[g].display_range.end)\n };\n this.data[g].display_domain.width = this.data[g].display_domain.end - this.data[g].display_domain.start;\n\n // Using display range/domain data generated above cast each gene to tracks such that none overlap\n this.data[g].track = null;\n var potential_track = 1;\n while (this.data[g].track === null){\n var collision_on_potential_track = false;\n this.gene_track_index[potential_track].map(function(placed_gene){\n if (!collision_on_potential_track){\n var min_start = Math.min(placed_gene.display_range.start, this.display_range.start);\n var max_end = Math.max(placed_gene.display_range.end, this.display_range.end);\n if ((max_end - min_start) < (placed_gene.display_range.width + this.display_range.width)){\n collision_on_potential_track = true;\n }\n }\n }.bind(this.data[g]));\n if (!collision_on_potential_track){\n this.data[g].track = potential_track;\n this.gene_track_index[potential_track].push(this.data[g]);\n } else {\n potential_track++;\n if (potential_track > this.tracks){\n this.tracks = potential_track;\n this.gene_track_index[potential_track] = [];\n }\n }\n }\n\n // Stash parent references on all genes, trascripts, and exons\n this.data[g].parent = this;\n this.data[g].transcripts.map(function(d, t){\n this.data[g].transcripts[t].parent = this.data[g];\n this.data[g].transcripts[t].exons.map(function(d, e){\n this.data[g].transcripts[t].exons[e].parent = this.data[g].transcripts[t];\n }.bind(this));\n }.bind(this));\n\n }.bind(this));\n return this;\n };\n\n /**\n * Main render function\n */\n this.render = function(){\n\n this.assignTracks();\n\n var width, height, x, y;\n\n // Render gene groups\n var selection = this.svg.group.selectAll(\"g.lz-data_layer-genes\")\n .data(this.data, function(d){ return d.gene_name; });\n\n selection.enter().append(\"g\")\n .attr(\"class\", \"lz-data_layer-genes\");\n \n selection.attr(\"id\", function(d){ return this.getElementId(d); }.bind(this))\n .each(function(gene){\n\n var data_layer = gene.parent;\n\n // Render gene bounding boxes (status nodes to show selected/highlighted)\n var bboxes = d3.select(this).selectAll(\"rect.lz-data_layer-genes.lz-data_layer-genes-statusnode\")\n .data([gene], function(d){ return data_layer.getElementStatusNodeId(d); });\n\n bboxes.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genes lz-data_layer-genes-statusnode\");\n \n bboxes\n .attr(\"id\", function(d){\n return data_layer.getElementStatusNodeId(d);\n })\n .attr(\"rx\", function(){\n return data_layer.layout.bounding_box_padding;\n })\n .attr(\"ry\", function(){\n return data_layer.layout.bounding_box_padding;\n });\n\n width = function(d){\n return d.display_range.width;\n };\n height = function(){\n return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;\n };\n x = function(d){\n return d.display_range.start;\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight());\n };\n if (data_layer.canTransition()){\n bboxes\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n bboxes\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n\n bboxes.exit().remove();\n\n // Render gene boundaries\n var boundaries = d3.select(this).selectAll(\"rect.lz-data_layer-genes.lz-boundary\")\n .data([gene], function(d){ return d.gene_name + \"_boundary\"; });\n\n boundaries.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genes lz-boundary\");\n\n width = function(d){\n return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start);\n };\n height = function(){\n return 1; // TODO: scale dynamically?\n };\n x = function(d){\n return data_layer.parent.x_scale(d.start);\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight())\n + data_layer.layout.bounding_box_padding\n + data_layer.layout.label_font_size\n + data_layer.layout.label_exon_spacing\n + (Math.max(data_layer.layout.exon_height, 3) / 2);\n };\n if (data_layer.canTransition()){\n boundaries\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n boundaries\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n \n boundaries.exit().remove();\n\n // Render gene labels\n var labels = d3.select(this).selectAll(\"text.lz-data_layer-genes.lz-label\")\n .data([gene], function(d){ return d.gene_name + \"_label\"; });\n\n labels.enter().append(\"text\")\n .attr(\"class\", \"lz-data_layer-genes lz-label\");\n\n labels\n .attr(\"text-anchor\", function(d){\n return d.display_range.text_anchor;\n })\n .text(function(d){\n return (d.strand === \"+\") ? d.gene_name + \"→\" : \"←\" + d.gene_name;\n })\n .style(\"font-size\", gene.parent.layout.label_font_size);\n\n x = function(d){\n if (d.display_range.text_anchor === \"middle\"){\n return d.display_range.start + (d.display_range.width / 2);\n } else if (d.display_range.text_anchor === \"start\"){\n return d.display_range.start + data_layer.layout.bounding_box_padding;\n } else if (d.display_range.text_anchor === \"end\"){\n return d.display_range.end - data_layer.layout.bounding_box_padding;\n }\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight())\n + data_layer.layout.bounding_box_padding\n + data_layer.layout.label_font_size;\n };\n if (data_layer.canTransition()){\n labels\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"x\", x).attr(\"y\", y);\n } else {\n labels\n .attr(\"x\", x).attr(\"y\", y);\n }\n\n labels.exit().remove();\n\n // Render exon rects (first transcript only, for now)\n var exons = d3.select(this).selectAll(\"rect.lz-data_layer-genes.lz-exon\")\n .data(gene.transcripts[gene.parent.transcript_idx].exons, function(d){ return d.exon_id; });\n \n exons.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genes lz-exon\");\n \n width = function(d){\n return data_layer.parent.x_scale(d.end) - data_layer.parent.x_scale(d.start);\n };\n height = function(){\n return data_layer.layout.exon_height;\n };\n x = function(d){\n return data_layer.parent.x_scale(d.start);\n };\n y = function(){\n return ((gene.track-1) * data_layer.getTrackHeight())\n + data_layer.layout.bounding_box_padding\n + data_layer.layout.label_font_size\n + data_layer.layout.label_exon_spacing;\n };\n if (data_layer.canTransition()){\n exons\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n exons\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n\n exons.exit().remove();\n\n // Render gene click area\n var clickareas = d3.select(this).selectAll(\"rect.lz-data_layer-genes.lz-clickarea\")\n .data([gene], function(d){ return d.gene_name + \"_clickarea\"; });\n\n clickareas.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genes lz-clickarea\");\n\n clickareas\n .attr(\"id\", function(d){\n return data_layer.getElementId(d) + \"_clickarea\";\n })\n .attr(\"rx\", function(){\n return data_layer.layout.bounding_box_padding;\n })\n .attr(\"ry\", function(){\n return data_layer.layout.bounding_box_padding;\n });\n\n width = function(d){\n return d.display_range.width;\n };\n height = function(){\n return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;\n };\n x = function(d){\n return d.display_range.start;\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight());\n };\n if (data_layer.canTransition()){\n clickareas\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n clickareas\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n\n // Remove old clickareas as needed\n clickareas.exit().remove();\n\n // Apply default event emitters to clickareas\n clickareas.on(\"click.event_emitter\", function(element){\n element.parent.parent.emit(\"element_clicked\", element);\n element.parent.parent_plot.emit(\"element_clicked\", element);\n });\n\n // Apply mouse behaviors to clickareas\n data_layer.applyBehaviors(clickareas);\n\n });\n\n // Remove old elements as needed\n selection.exit().remove();\n\n };\n\n /**\n * Reimplement the positionTooltip() method to be gene-specific\n * @param {String} id\n */\n this.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]){\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var tooltip = this.tooltips[id];\n var arrow_width = 7; // as defined in the default stylesheet\n var stroke_width = 1; // as defined in the default stylesheet\n var page_origin = this.getPageOrigin();\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n var gene_bbox_id = this.getElementStatusNodeId(tooltip.data);\n var gene_bbox = d3.select(\"#\" + gene_bbox_id).node().getBBox();\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right);\n // Position horizontally: attempt to center on the portion of the gene that's visible,\n // pad to either side if bumping up against the edge of the data layer\n var gene_center_x = ((tooltip.data.display_range.start + tooltip.data.display_range.end) / 2) - (this.layout.bounding_box_padding / 2);\n var offset_right = Math.max((tooltip_box.width / 2) - gene_center_x, 0);\n var offset_left = Math.max((tooltip_box.width / 2) + gene_center_x - data_layer_width, 0);\n var left = page_origin.x + gene_center_x - (tooltip_box.width / 2) - offset_left + offset_right;\n var arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right;\n // Position vertically below the gene unless there's insufficient space\n var top, arrow_type, arrow_top;\n if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (gene_bbox.y + gene_bbox.height)){\n top = page_origin.y + gene_bbox.y - (tooltip_box.height + stroke_width + arrow_width);\n arrow_type = \"down\";\n arrow_top = tooltip_box.height - stroke_width;\n } else {\n top = page_origin.y + gene_bbox.y + gene_bbox.height + stroke_width + arrow_width;\n arrow_type = \"up\";\n arrow_top = 0 - stroke_width - arrow_width;\n }\n // Apply positions to the main div\n tooltip.selector.style(\"left\", left + \"px\").style(\"top\", top + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow){\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style(\"left\", arrow_left + \"px\")\n .style(\"top\", arrow_top + \"px\");\n };\n \n return this;\n\n});\n","\"use strict\";\n\n/*********************\n Genome Legend Data Layer\n Implements a data layer that will render a genome legend\n*/\n\n// Build a custom data layer for a genome legend\nLocusZoom.DataLayers.add(\"genome_legend\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n this.DefaultLayout = {\n chromosome_fill_colors: {\n light: \"rgb(155, 155, 188)\",\n dark: \"rgb(95, 95, 128)\"\n },\n chromosome_label_colors: {\n light: \"rgb(120, 120, 186)\",\n dark: \"rgb(0, 0, 66)\"\n }\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n // Implement the main render function\n this.render = function(){\n\n // Iterate over data to generate genome-wide start/end values for each chromosome\n var position = 0;\n this.data.forEach(function(d, i){\n this.data[i].genome_start = position;\n this.data[i].genome_end = position + d[\"genome:base_pairs\"];\n position += d[\"genome:base_pairs\"];\n }.bind(this));\n\n var chromosomes = this.svg.group\n .selectAll(\"rect.lz-data_layer-genome_legend\")\n .data(this.data, function(d){ return d[\"genome:chr\"]; });\n\n // Create chromosome elements, apply class\n chromosomes.enter()\n .append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genome_legend\");\n\n // Position and fill chromosome rects\n var data_layer = this;\n var panel = this.parent;\n\n chromosomes\n .attr(\"fill\", function(d){ return (d[\"genome:chr\"] % 2 ? data_layer.layout.chromosome_fill_colors.light : data_layer.layout.chromosome_fill_colors.dark); })\n .attr(\"x\", function(d){ return panel.x_scale(d.genome_start); })\n .attr(\"y\", 0)\n .attr(\"width\", function(d){ return panel.x_scale(d[\"genome:base_pairs\"]); })\n .attr(\"height\", panel.layout.cliparea.height);\n\n // Remove old elements as needed\n chromosomes.exit().remove();\n\n // Parse current state variant into a position\n // Assumes that variant string is of the format 10:123352136_C/T or 10:123352136\n var variant_parts = /([^:]+):(\\d+)(?:_.*)?/.exec(this.state.variant);\n if (!variant_parts) {\n throw(\"Genome legend cannot understand the specified variant position\");\n }\n var chr = variant_parts[1];\n var offset = variant_parts[2];\n // TODO: How does this handle representation of X or Y chromosomes?\n position = +this.data[chr-1].genome_start + +offset;\n\n // Render the position\n var region = this.svg.group\n .selectAll(\"rect.lz-data_layer-genome_legend-marker\")\n .data([{ start: position, end: position + 1 }]);\n\n region.enter()\n .append(\"rect\")\n .attr(\"class\", \"lz-data_layer-genome_legend-marker\");\n\n region\n .transition()\n .duration(500)\n .style({\n \"fill\": \"rgba(255, 250, 50, 0.8)\",\n \"stroke\": \"rgba(255, 250, 50, 0.8)\",\n \"stroke-width\": \"3px\"\n })\n .attr(\"x\", function(d){ return panel.x_scale(d.start); })\n .attr(\"y\", 0)\n .attr(\"width\", function(d){ return panel.x_scale(d.end - d.start); })\n .attr(\"height\", panel.layout.cliparea.height);\n\n region.exit().remove();\n \n };\n \n return this;\n\n});\n","\"use strict\";\n\n/**\n * Intervals Data Layer\n * Implements a data layer that will render interval annotation tracks (intervals must provide start and end values)\n * @class LocusZoom.DataLayers.intervals\n * @augments LocusZoom.DataLayer\n */\nLocusZoom.DataLayers.add(\"intervals\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n this.DefaultLayout = {\n start_field: \"start\",\n end_field: \"end\",\n track_split_field: \"state_id\",\n track_split_order: \"DESC\",\n track_split_legend_to_y_axis: 2,\n split_tracks: true,\n track_height: 15,\n track_vertical_spacing: 3,\n bounding_box_padding: 2,\n always_hide_legend: false,\n color: \"#B8B8B8\",\n fill_opacity: 1\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n \n /**\n * To define shared highlighting on the track split field define the status node id override\n * to generate an ID common to the track when we're actively splitting data out to separate tracks\n * @override\n * @returns {String}\n */\n this.getElementStatusNodeId = function(element){\n if (this.layout.split_tracks){\n return (this.getBaseId() + \"-statusnode-\" + element[this.layout.track_split_field]).replace(/[:.[\\],]/g, \"_\");\n }\n return this.getElementId(element) + \"-statusnode\";\n }.bind(this);\n \n // Helper function to sum layout values to derive total height for a single interval track\n this.getTrackHeight = function(){\n return this.layout.track_height\n + this.layout.track_vertical_spacing\n + (2 * this.layout.bounding_box_padding);\n };\n\n this.tracks = 1;\n this.previous_tracks = 1;\n \n // track-number-indexed object with arrays of interval indexes in the dataset\n this.interval_track_index = { 1: [] };\n\n // After we've loaded interval data interpret it to assign\n // each to a track so that they do not overlap in the view\n this.assignTracks = function(){\n\n // Reinitialize some metadata\n this.previous_tracks = this.tracks;\n this.tracks = 0;\n this.interval_track_index = { 1: [] };\n this.track_split_field_index = {};\n \n // If splitting tracks by a field's value then do a first pass determine\n // a value/track mapping that preserves the order of possible values\n if (this.layout.track_split_field && this.layout.split_tracks){\n this.data.map(function(d){\n this.track_split_field_index[d[this.layout.track_split_field]] = null;\n }.bind(this));\n var index = Object.keys(this.track_split_field_index);\n if (this.layout.track_split_order === \"DESC\"){ index.reverse(); }\n index.forEach(function(val){\n this.track_split_field_index[val] = this.tracks + 1;\n this.interval_track_index[this.tracks + 1] = [];\n this.tracks++;\n }.bind(this));\n }\n\n this.data.map(function(d, i){\n\n // Stash a parent reference on the interval\n this.data[i].parent = this;\n\n // Determine display range start and end, based on minimum allowable interval display width,\n // bounded by what we can see (range: values in terms of pixels on the screen)\n this.data[i].display_range = {\n start: this.parent.x_scale(Math.max(d[this.layout.start_field], this.state.start)),\n end: this.parent.x_scale(Math.min(d[this.layout.end_field], this.state.end))\n };\n this.data[i].display_range.width = this.data[i].display_range.end - this.data[i].display_range.start;\n \n // Convert and stash display range values into domain values\n // (domain: values in terms of the data set, e.g. megabases)\n this.data[i].display_domain = {\n start: this.parent.x_scale.invert(this.data[i].display_range.start),\n end: this.parent.x_scale.invert(this.data[i].display_range.end)\n };\n this.data[i].display_domain.width = this.data[i].display_domain.end - this.data[i].display_domain.start;\n\n // If splitting to tracks based on the value of the designated track split field\n // then don't bother with collision detection (intervals will be grouped on tracks\n // solely by the value of track_split_field)\n if (this.layout.track_split_field && this.layout.split_tracks){\n var val = this.data[i][this.layout.track_split_field];\n this.data[i].track = this.track_split_field_index[val];\n this.interval_track_index[this.data[i].track].push(i);\n } else {\n // If not splitting to tracks based on a field value then do so based on collision\n // detection (as how it's done for genes). Use display range/domain data generated\n // above and cast each interval to tracks such that none overlap\n this.tracks = 1;\n this.data[i].track = null;\n var potential_track = 1;\n while (this.data[i].track === null){\n var collision_on_potential_track = false;\n this.interval_track_index[potential_track].map(function(placed_interval){\n if (!collision_on_potential_track){\n var min_start = Math.min(placed_interval.display_range.start, this.display_range.start);\n var max_end = Math.max(placed_interval.display_range.end, this.display_range.end);\n if ((max_end - min_start) < (placed_interval.display_range.width + this.display_range.width)){\n collision_on_potential_track = true;\n }\n }\n }.bind(this.data[i]));\n if (!collision_on_potential_track){\n this.data[i].track = potential_track;\n this.interval_track_index[potential_track].push(this.data[i]);\n } else {\n potential_track++;\n if (potential_track > this.tracks){\n this.tracks = potential_track;\n this.interval_track_index[potential_track] = [];\n }\n }\n }\n\n }\n\n }.bind(this));\n\n return this;\n };\n\n // Implement the main render function\n this.render = function(){\n\n this.assignTracks();\n\n // Remove any shared highlight nodes and re-render them if we're splitting on tracks\n // At most there will only be dozen or so nodes here (one per track) and each time\n // we render data we may have new tracks, so wiping/redrawing all is reasonable.\n this.svg.group.selectAll(\".lz-data_layer-intervals-statusnode.lz-data_layer-intervals-shared\").remove();\n Object.keys(this.track_split_field_index).forEach(function(key){\n // Make a psuedo-element so that we can generate an id for the shared node\n var psuedoElement = {};\n psuedoElement[this.layout.track_split_field] = key;\n // Insert the shared node\n var sharedstatusnode_style = {display: (this.layout.split_tracks ? null : \"none\")};\n this.svg.group.insert(\"rect\", \":first-child\")\n .attr(\"id\", this.getElementStatusNodeId(psuedoElement))\n .attr(\"class\", \"lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-shared\")\n .attr(\"rx\", this.layout.bounding_box_padding).attr(\"ry\", this.layout.bounding_box_padding)\n .attr(\"width\", this.parent.layout.cliparea.width)\n .attr(\"height\", this.getTrackHeight() - this.layout.track_vertical_spacing)\n .attr(\"x\", 0)\n .attr(\"y\", (this.track_split_field_index[key]-1) * this.getTrackHeight())\n .style(sharedstatusnode_style);\n }.bind(this));\n\n var width, height, x, y, fill, fill_opacity;\n \n // Render interval groups\n var selection = this.svg.group.selectAll(\"g.lz-data_layer-intervals\")\n .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this));\n\n selection.enter().append(\"g\")\n .attr(\"class\", \"lz-data_layer-intervals\");\n \n selection.attr(\"id\", function(d){ return this.getElementId(d); }.bind(this))\n .each(function(interval){\n\n var data_layer = interval.parent;\n\n // Render interval status nodes (displayed behind intervals to show highlight\n // without needing to modify interval display element(s))\n var statusnode_style = {display: (data_layer.layout.split_tracks ? \"none\" : null)};\n var statusnodes = d3.select(this).selectAll(\"rect.lz-data_layer-intervals.lz-data_layer-intervals-statusnode.lz-data_layer-intervals-statusnode-discrete\")\n .data([interval], function(d){ return data_layer.getElementId(d) + \"-statusnode\"; });\n statusnodes.enter().insert(\"rect\", \":first-child\")\n .attr(\"class\", \"lz-data_layer-intervals lz-data_layer-intervals-statusnode lz-data_layer-intervals-statusnode-discrete\");\n statusnodes\n .attr(\"id\", function(d){\n return data_layer.getElementId(d) + \"-statusnode\";\n })\n .attr(\"rx\", function(){\n return data_layer.layout.bounding_box_padding;\n })\n .attr(\"ry\", function(){\n return data_layer.layout.bounding_box_padding;\n })\n .style(statusnode_style);\n width = function(d){\n return d.display_range.width + (2 * data_layer.layout.bounding_box_padding);\n };\n height = function(){\n return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;\n };\n x = function(d){\n return d.display_range.start - data_layer.layout.bounding_box_padding;\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight());\n };\n if (data_layer.canTransition()){\n statusnodes\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n statusnodes\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n statusnodes.exit().remove();\n\n // Render primary interval rects\n var rects = d3.select(this).selectAll(\"rect.lz-data_layer-intervals.lz-interval_rect\")\n .data([interval], function(d){ return d[data_layer.layout.id_field] + \"_interval_rect\"; });\n\n rects.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-intervals lz-interval_rect\");\n\n height = data_layer.layout.track_height;\n width = function(d){\n return d.display_range.width;\n };\n x = function(d){\n return d.display_range.start;\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight())\n + data_layer.layout.bounding_box_padding;\n };\n fill = function(d){\n return data_layer.resolveScalableParameter(data_layer.layout.color, d);\n };\n fill_opacity = function(d){\n return data_layer.resolveScalableParameter(data_layer.layout.fill_opacity, d);\n };\n \n \n if (data_layer.canTransition()){\n rects\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height)\n .attr(\"x\", x).attr(\"y\", y)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity);\n } else {\n rects\n .attr(\"width\", width).attr(\"height\", height)\n .attr(\"x\", x).attr(\"y\", y)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity);\n }\n \n rects.exit().remove();\n\n // Render interval click areas\n var clickareas = d3.select(this).selectAll(\"rect.lz-data_layer-intervals.lz-clickarea\")\n .data([interval], function(d){ return d.interval_name + \"_clickarea\"; });\n\n clickareas.enter().append(\"rect\")\n .attr(\"class\", \"lz-data_layer-intervals lz-clickarea\");\n\n clickareas\n .attr(\"id\", function(d){\n return data_layer.getElementId(d) + \"_clickarea\";\n })\n .attr(\"rx\", function(){\n return data_layer.layout.bounding_box_padding;\n })\n .attr(\"ry\", function(){\n return data_layer.layout.bounding_box_padding;\n });\n\n width = function(d){\n return d.display_range.width;\n };\n height = function(){\n return data_layer.getTrackHeight() - data_layer.layout.track_vertical_spacing;\n };\n x = function(d){\n return d.display_range.start;\n };\n y = function(d){\n return ((d.track-1) * data_layer.getTrackHeight());\n };\n if (data_layer.canTransition()){\n clickareas\n .transition()\n .duration(data_layer.layout.transition.duration || 0)\n .ease(data_layer.layout.transition.ease || \"cubic-in-out\")\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n } else {\n clickareas\n .attr(\"width\", width).attr(\"height\", height).attr(\"x\", x).attr(\"y\", y);\n }\n\n // Remove old clickareas as needed\n clickareas.exit().remove();\n\n // Apply default event emitters to clickareas\n clickareas.on(\"click\", function(element){\n element.parent.parent.emit(\"element_clicked\", element);\n element.parent.parent_plot.emit(\"element_clicked\", element);\n }.bind(this));\n\n // Apply mouse behaviors to clickareas\n data_layer.applyBehaviors(clickareas);\n\n });\n\n // Remove old elements as needed\n selection.exit().remove();\n\n // Update the legend axis if the number of ticks changed\n if (this.previous_tracks !== this.tracks){\n this.updateSplitTrackAxis();\n }\n\n return this;\n\n };\n \n // Reimplement the positionTooltip() method to be interval-specific\n this.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]){\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var tooltip = this.tooltips[id];\n var arrow_width = 7; // as defined in the default stylesheet\n var stroke_width = 1; // as defined in the default stylesheet\n var page_origin = this.getPageOrigin();\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n var interval_bbox = d3.select(\"#\" + this.getElementStatusNodeId(tooltip.data)).node().getBBox();\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right);\n // Position horizontally: attempt to center on the portion of the interval that's visible,\n // pad to either side if bumping up against the edge of the data layer\n var interval_center_x = ((tooltip.data.display_range.start + tooltip.data.display_range.end) / 2) - (this.layout.bounding_box_padding / 2);\n var offset_right = Math.max((tooltip_box.width / 2) - interval_center_x, 0);\n var offset_left = Math.max((tooltip_box.width / 2) + interval_center_x - data_layer_width, 0);\n var left = page_origin.x + interval_center_x - (tooltip_box.width / 2) - offset_left + offset_right;\n var arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right;\n // Position vertically below the interval unless there's insufficient space\n var top, arrow_type, arrow_top;\n if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (interval_bbox.y + interval_bbox.height)){\n top = page_origin.y + interval_bbox.y - (tooltip_box.height + stroke_width + arrow_width);\n arrow_type = \"down\";\n arrow_top = tooltip_box.height - stroke_width;\n } else {\n top = page_origin.y + interval_bbox.y + interval_bbox.height + stroke_width + arrow_width;\n arrow_type = \"up\";\n arrow_top = 0 - stroke_width - arrow_width;\n }\n // Apply positions to the main div\n tooltip.selector.style(\"left\", left + \"px\").style(\"top\", top + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow){\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style(\"left\", arrow_left + \"px\")\n .style(\"top\", arrow_top + \"px\");\n };\n\n // Redraw split track axis or hide it, and show/hide the legend, as determined\n // by current layout parameters and data\n this.updateSplitTrackAxis = function(){\n var legend_axis = this.layout.track_split_legend_to_y_axis ? \"y\" + this.layout.track_split_legend_to_y_axis : false;\n if (this.layout.split_tracks){\n var tracks = +this.tracks || 0;\n var track_height = +this.layout.track_height || 0;\n var track_spacing = 2 * (+this.layout.bounding_box_padding || 0) + (+this.layout.track_vertical_spacing || 0);\n var target_height = (tracks * track_height) + ((tracks - 1) * track_spacing);\n this.parent.scaleHeightToData(target_height);\n if (legend_axis && this.parent.legend){\n this.parent.legend.hide(); \n this.parent.layout.axes[legend_axis] = {\n render: true,\n ticks: [],\n range: {\n start: (target_height - (this.layout.track_height/2)),\n end: (this.layout.track_height/2)\n }\n };\n this.layout.legend.forEach(function(element){\n var key = element[this.layout.track_split_field];\n var track = this.track_split_field_index[key];\n if (track){\n if (this.layout.track_split_order === \"DESC\"){\n track = Math.abs(track - tracks - 1);\n }\n this.parent.layout.axes[legend_axis].ticks.push({\n y: track,\n text: element.label\n });\n }\n }.bind(this));\n this.layout.y_axis = {\n axis: this.layout.track_split_legend_to_y_axis,\n floor: 1,\n ceiling: tracks\n };\n this.parent.render();\n }\n this.parent_plot.positionPanels();\n } else {\n if (legend_axis && this.parent.legend){\n if (!this.layout.always_hide_legend){ this.parent.legend.show(); }\n this.parent.layout.axes[legend_axis] = { render: false };\n this.parent.render();\n }\n }\n return this;\n };\n\n // Method to not only toggle the split tracks boolean but also update\n // necessary display values to animate a complete merge/split\n this.toggleSplitTracks = function(){\n this.layout.split_tracks = !this.layout.split_tracks;\n if (this.parent.legend && !this.layout.always_hide_legend){\n this.parent.layout.margin.bottom = 5 + (this.layout.split_tracks ? 0 : this.parent.legend.layout.height + 5);\n }\n this.render();\n this.updateSplitTrackAxis();\n return this;\n };\n \n return this;\n\n});\n","\"use strict\";\n\n/*********************\n * Line Data Layer\n * Implements a standard line plot\n * @class\n * @augments LocusZoom.DataLayer\n*/\nLocusZoom.DataLayers.add(\"line\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n /** @member {Object} */\n this.DefaultLayout = {\n style: {\n fill: \"none\",\n \"stroke-width\": \"2px\"\n },\n interpolate: \"linear\",\n x_axis: { field: \"x\" },\n y_axis: { field: \"y\", axis: 1 },\n hitarea_width: 5\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Var for storing mouse events for use in tool tip positioning\n /** @member {String} */\n this.mouse_event = null;\n\n /**\n * Var for storing the generated line function itself\n * @member {d3.svg.line}\n * */\n this.line = null;\n\n /**\n * The timeout identifier returned by setTimeout\n * @member {Number}\n */\n this.tooltip_timeout = null;\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n\n /**\n * Helper function to get display and data objects representing\n * the x/y coordinates of the current mouse event with respect to the line in terms of the display\n * and the interpolated values of the x/y fields with respect to the line\n * @returns {{display: {x: *, y: null}, data: {}, slope: null}}\n */\n this.getMouseDisplayAndData = function(){\n var ret = {\n display: {\n x: d3.mouse(this.mouse_event)[0],\n y: null\n },\n data: {},\n slope: null\n };\n var x_field = this.layout.x_axis.field;\n var y_field = this.layout.y_axis.field;\n var x_scale = \"x_scale\";\n var y_scale = \"y\" + this.layout.y_axis.axis + \"_scale\";\n ret.data[x_field] = this.parent[x_scale].invert(ret.display.x);\n var bisect = d3.bisector(function(datum) { return +datum[x_field]; }).left;\n var index = bisect(this.data, ret.data[x_field]) - 1;\n var startDatum = this.data[index];\n var endDatum = this.data[index + 1];\n var interpolate = d3.interpolateNumber(+startDatum[y_field], +endDatum[y_field]);\n var range = +endDatum[x_field] - +startDatum[x_field];\n ret.data[y_field] = interpolate((ret.data[x_field] % range) / range);\n ret.display.y = this.parent[y_scale](ret.data[y_field]);\n if (this.layout.tooltip.x_precision){\n ret.data[x_field] = ret.data[x_field].toPrecision(this.layout.tooltip.x_precision);\n }\n if (this.layout.tooltip.y_precision){\n ret.data[y_field] = ret.data[y_field].toPrecision(this.layout.tooltip.y_precision);\n }\n ret.slope = (this.parent[y_scale](endDatum[y_field]) - this.parent[y_scale](startDatum[y_field]))\n / (this.parent[x_scale](endDatum[x_field]) - this.parent[x_scale](startDatum[x_field]));\n return ret;\n };\n\n /**\n * Reimplement the positionTooltip() method to be line-specific\n * @param {String} id Identify the tooltip to be positioned\n */\n this.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]){\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var tooltip = this.tooltips[id];\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n var arrow_width = 7; // as defined in the default stylesheet\n var border_radius = 6; // as defined in the default stylesheet\n var stroke_width = parseFloat(this.layout.style[\"stroke-width\"]) || 1;\n var page_origin = this.getPageOrigin();\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right);\n var top, left, arrow_top, arrow_left, arrow_type;\n\n // Determine x/y coordinates for display and data\n var dd = this.getMouseDisplayAndData();\n\n // If the absolute value of the slope of the line at this point is above 1 (including Infinity)\n // then position the tool tip left/right. Otherwise position top/bottom.\n if (Math.abs(dd.slope) > 1){\n\n // Position horizontally on the left or the right depending on which side of the plot the point is on\n if (dd.display.x <= this.parent.layout.width / 2){\n left = page_origin.x + dd.display.x + stroke_width + arrow_width + stroke_width;\n arrow_type = \"left\";\n arrow_left = -1 * (arrow_width + stroke_width);\n } else {\n left = page_origin.x + dd.display.x - tooltip_box.width - stroke_width - arrow_width - stroke_width;\n arrow_type = \"right\";\n arrow_left = tooltip_box.width - stroke_width;\n }\n // Position vertically centered unless we're at the top or bottom of the plot\n if (dd.display.y - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down\n top = page_origin.y + dd.display.y - (1.5 * arrow_width) - border_radius;\n arrow_top = border_radius;\n } else if (dd.display.y + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up\n top = page_origin.y + dd.display.y + arrow_width + border_radius - tooltip_box.height;\n arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius;\n } else { // vertically centered\n top = page_origin.y + dd.display.y - (tooltip_box.height / 2);\n arrow_top = (tooltip_box.height / 2) - arrow_width;\n }\n\n } else {\n\n // Position horizontally: attempt to center on the mouse's x coordinate\n // pad to either side if bumping up against the edge of the data layer\n var offset_right = Math.max((tooltip_box.width / 2) - dd.display.x, 0);\n var offset_left = Math.max((tooltip_box.width / 2) + dd.display.x - data_layer_width, 0);\n left = page_origin.x + dd.display.x - (tooltip_box.width / 2) - offset_left + offset_right;\n var min_arrow_left = arrow_width / 2;\n var max_arrow_left = tooltip_box.width - (2.5 * arrow_width);\n arrow_left = (tooltip_box.width / 2) - arrow_width + offset_left - offset_right;\n arrow_left = Math.min(Math.max(arrow_left, min_arrow_left), max_arrow_left);\n\n // Position vertically above the line unless there's insufficient space\n if (tooltip_box.height + stroke_width + arrow_width > dd.display.y){\n top = page_origin.y + dd.display.y + stroke_width + arrow_width;\n arrow_type = \"up\";\n arrow_top = 0 - stroke_width - arrow_width;\n } else {\n top = page_origin.y + dd.display.y - (tooltip_box.height + stroke_width + arrow_width);\n arrow_type = \"down\";\n arrow_top = tooltip_box.height - stroke_width;\n }\n }\n\n // Apply positions to the main div\n tooltip.selector.style({ left: left + \"px\", top: top + \"px\" });\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow){\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style({ \"left\": arrow_left + \"px\", top: arrow_top + \"px\" });\n\n };\n\n /**\n * Implement the main render function\n */\n this.render = function(){\n\n // Several vars needed to be in scope\n var data_layer = this;\n var panel = this.parent;\n var x_field = this.layout.x_axis.field;\n var y_field = this.layout.y_axis.field;\n var x_scale = \"x_scale\";\n var y_scale = \"y\" + this.layout.y_axis.axis + \"_scale\";\n\n // Join data to the line selection\n var selection = this.svg.group\n .selectAll(\"path.lz-data_layer-line\")\n .data([this.data]);\n\n // Create path element, apply class\n this.path = selection.enter()\n .append(\"path\")\n .attr(\"class\", \"lz-data_layer-line\");\n\n // Generate the line\n this.line = d3.svg.line()\n .x(function(d) { return parseFloat(panel[x_scale](d[x_field])); })\n .y(function(d) { return parseFloat(panel[y_scale](d[y_field])); })\n .interpolate(this.layout.interpolate);\n\n // Apply line and style\n if (this.canTransition()){\n selection\n .transition()\n .duration(this.layout.transition.duration || 0)\n .ease(this.layout.transition.ease || \"cubic-in-out\")\n .attr(\"d\", this.line)\n .style(this.layout.style);\n } else {\n selection\n .attr(\"d\", this.line)\n .style(this.layout.style);\n }\n\n // Apply tooltip, etc\n if (this.layout.tooltip){\n // Generate an overlaying transparent \"hit area\" line for more intuitive mouse events\n var hitarea_width = parseFloat(this.layout.hitarea_width).toString() + \"px\";\n var hitarea = this.svg.group\n .selectAll(\"path.lz-data_layer-line-hitarea\")\n .data([this.data]);\n hitarea.enter()\n .append(\"path\")\n .attr(\"class\", \"lz-data_layer-line-hitarea\")\n .style(\"stroke-width\", hitarea_width);\n var hitarea_line = d3.svg.line()\n .x(function(d) { return parseFloat(panel[x_scale](d[x_field])); })\n .y(function(d) { return parseFloat(panel[y_scale](d[y_field])); })\n .interpolate(this.layout.interpolate);\n hitarea\n .attr(\"d\", hitarea_line)\n .on(\"mouseover\", function(){\n clearTimeout(data_layer.tooltip_timeout);\n data_layer.mouse_event = this;\n var dd = data_layer.getMouseDisplayAndData();\n data_layer.createTooltip(dd.data);\n })\n .on(\"mousemove\", function(){\n clearTimeout(data_layer.tooltip_timeout);\n data_layer.mouse_event = this;\n var dd = data_layer.getMouseDisplayAndData();\n data_layer.updateTooltip(dd.data);\n data_layer.positionTooltip(data_layer.getElementId());\n })\n .on(\"mouseout\", function(){\n data_layer.tooltip_timeout = setTimeout(function(){\n data_layer.mouse_event = null;\n data_layer.destroyTooltip(data_layer.getElementId());\n }, 300);\n });\n hitarea.exit().remove();\n }\n\n // Remove old elements as needed\n selection.exit().remove();\n \n };\n\n /**\n * Redefine setElementStatus family of methods as line data layers will only ever have a single path element\n * @param {String} status A member of `LocusZoom.DataLayer.Statuses.adjectives`\n * @param {String|Object} element\n * @param {Boolean} toggle\n * @returns {LocusZoom.DataLayer}\n */\n this.setElementStatus = function(status, element, toggle){\n return this.setAllElementStatus(status, toggle);\n };\n this.setElementStatusByFilters = function(status, toggle){\n return this.setAllElementStatus(status, toggle);\n };\n this.setAllElementStatus = function(status, toggle){\n // Sanity check\n if (typeof status == \"undefined\" || LocusZoom.DataLayer.Statuses.adjectives.indexOf(status) === -1){\n throw(\"Invalid status passed to DataLayer.setAllElementStatus()\");\n }\n if (typeof this.state[this.state_id][status] == \"undefined\"){ return this; }\n if (typeof toggle == \"undefined\"){ toggle = true; }\n\n // Update global status flag\n this.global_statuses[status] = toggle;\n\n // Apply class to path based on global status flags\n var path_class = \"lz-data_layer-line\";\n Object.keys(this.global_statuses).forEach(function(global_status){\n if (this.global_statuses[global_status]){ path_class += \" lz-data_layer-line-\" + global_status; }\n }.bind(this));\n this.path.attr(\"class\", path_class);\n\n // Trigger layout changed event hook\n this.parent.emit(\"layout_changed\");\n this.parent_plot.emit(\"layout_changed\");\n \n return this;\n };\n\n return this;\n\n});\n\n\n/***************************\n * Orthogonal Line Data Layer\n * Implements a horizontal or vertical line given an orientation and an offset in the layout\n * Does not require a data source\n * @class\n * @augments LocusZoom.DataLayer\n*/\nLocusZoom.DataLayers.add(\"orthogonal_line\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n this.DefaultLayout = {\n style: {\n \"stroke\": \"#D3D3D3\",\n \"stroke-width\": \"3px\",\n \"stroke-dasharray\": \"10px 10px\"\n },\n orientation: \"horizontal\",\n x_axis: {\n axis: 1,\n decoupled: true\n },\n y_axis: {\n axis: 1,\n decoupled: true\n },\n offset: 0\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Require that orientation be \"horizontal\" or \"vertical\" only\n if ([\"horizontal\",\"vertical\"].indexOf(layout.orientation) === -1){\n layout.orientation = \"horizontal\";\n }\n\n // Vars for storing the data generated line\n /** @member {Array} */\n this.data = [];\n /** @member {d3.svg.line} */\n this.line = null;\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n /**\n * Implement the main render function\n */\n this.render = function(){\n\n // Several vars needed to be in scope\n var panel = this.parent;\n var x_scale = \"x_scale\";\n var y_scale = \"y\" + this.layout.y_axis.axis + \"_scale\";\n var x_extent = \"x_extent\";\n var y_extent = \"y\" + this.layout.y_axis.axis + \"_extent\";\n var x_range = \"x_range\";\n var y_range = \"y\" + this.layout.y_axis.axis + \"_range\";\n\n // Generate data using extents depending on orientation\n if (this.layout.orientation === \"horizontal\"){\n this.data = [\n { x: panel[x_extent][0], y: this.layout.offset },\n { x: panel[x_extent][1], y: this.layout.offset }\n ];\n } else {\n this.data = [\n { x: this.layout.offset, y: panel[y_extent][0] },\n { x: this.layout.offset, y: panel[y_extent][1] }\n ];\n }\n\n // Join data to the line selection\n var selection = this.svg.group\n .selectAll(\"path.lz-data_layer-line\")\n .data([this.data]);\n\n // Create path element, apply class\n this.path = selection.enter()\n .append(\"path\")\n .attr(\"class\", \"lz-data_layer-line\");\n\n // Generate the line\n this.line = d3.svg.line()\n .x(function(d, i) {\n var x = parseFloat(panel[x_scale](d[\"x\"]));\n return isNaN(x) ? panel[x_range][i] : x;\n })\n .y(function(d, i) {\n var y = parseFloat(panel[y_scale](d[\"y\"]));\n return isNaN(y) ? panel[y_range][i] : y;\n })\n .interpolate(\"linear\");\n\n // Apply line and style\n if (this.canTransition()){\n selection\n .transition()\n .duration(this.layout.transition.duration || 0)\n .ease(this.layout.transition.ease || \"cubic-in-out\")\n .attr(\"d\", this.line)\n .style(this.layout.style);\n } else {\n selection\n .attr(\"d\", this.line)\n .style(this.layout.style);\n }\n\n // Remove old elements as needed\n selection.exit().remove();\n \n };\n\n return this;\n\n});\n","\"use strict\";\n\n/*********************\n Scatter Data Layer\n Implements a standard scatter plot\n*/\n\nLocusZoom.DataLayers.add(\"scatter\", function(layout){\n\n // Define a default layout for this DataLayer type and merge it with the passed argument\n this.DefaultLayout = {\n point_size: 40,\n point_shape: \"circle\",\n tooltip_positioning: \"horizontal\",\n color: \"#888888\",\n fill_opacity: 1,\n y_axis: {\n axis: 1\n },\n id_field: \"id\"\n };\n layout = LocusZoom.Layouts.merge(layout, this.DefaultLayout);\n\n // Extra default for layout spacing\n // Not in default layout since that would make the label attribute always present\n if (layout.label && isNaN(layout.label.spacing)){\n layout.label.spacing = 4;\n }\n\n // Apply the arguments to set LocusZoom.DataLayer as the prototype\n LocusZoom.DataLayer.apply(this, arguments);\n\n // Reimplement the positionTooltip() method to be scatter-specific\n this.positionTooltip = function(id){\n if (typeof id != \"string\"){\n throw (\"Unable to position tooltip: id is not a string\");\n }\n if (!this.tooltips[id]){\n throw (\"Unable to position tooltip: id does not point to a valid tooltip\");\n }\n var top, left, arrow_type, arrow_top, arrow_left;\n var tooltip = this.tooltips[id];\n var point_size = this.resolveScalableParameter(this.layout.point_size, tooltip.data);\n var offset = Math.sqrt(point_size / Math.PI);\n var arrow_width = 7; // as defined in the default stylesheet\n var stroke_width = 1; // as defined in the default stylesheet\n var border_radius = 6; // as defined in the default stylesheet\n var page_origin = this.getPageOrigin();\n var x_center = this.parent.x_scale(tooltip.data[this.layout.x_axis.field]);\n var y_scale = \"y\"+this.layout.y_axis.axis+\"_scale\";\n var y_center = this.parent[y_scale](tooltip.data[this.layout.y_axis.field]);\n var tooltip_box = tooltip.selector.node().getBoundingClientRect();\n var data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n var data_layer_width = this.parent.layout.width - (this.parent.layout.margin.left + this.parent.layout.margin.right);\n if (this.layout.tooltip_positioning === \"vertical\"){\n // Position horizontally centered above the point\n var offset_right = Math.max((tooltip_box.width / 2) - x_center, 0);\n var offset_left = Math.max((tooltip_box.width / 2) + x_center - data_layer_width, 0);\n left = page_origin.x + x_center - (tooltip_box.width / 2) - offset_left + offset_right;\n arrow_left = (tooltip_box.width / 2) - (arrow_width / 2) + offset_left - offset_right - offset;\n // Position vertically above the point unless there's insufficient space, then go below\n if (tooltip_box.height + stroke_width + arrow_width > data_layer_height - (y_center + offset)){\n top = page_origin.y + y_center - (offset + tooltip_box.height + stroke_width + arrow_width);\n arrow_type = \"down\";\n arrow_top = tooltip_box.height - stroke_width;\n } else {\n top = page_origin.y + y_center + offset + stroke_width + arrow_width;\n arrow_type = \"up\";\n arrow_top = 0 - stroke_width - arrow_width;\n }\n } else {\n // Position horizontally on the left or the right depending on which side of the plot the point is on\n if (x_center <= this.parent.layout.width / 2){\n left = page_origin.x + x_center + offset + arrow_width + stroke_width;\n arrow_type = \"left\";\n arrow_left = -1 * (arrow_width + stroke_width);\n } else {\n left = page_origin.x + x_center - tooltip_box.width - offset - arrow_width - stroke_width;\n arrow_type = \"right\";\n arrow_left = tooltip_box.width - stroke_width;\n }\n // Position vertically centered unless we're at the top or bottom of the plot\n data_layer_height = this.parent.layout.height - (this.parent.layout.margin.top + this.parent.layout.margin.bottom);\n if (y_center - (tooltip_box.height / 2) <= 0){ // Too close to the top, push it down\n top = page_origin.y + y_center - (1.5 * arrow_width) - border_radius;\n arrow_top = border_radius;\n } else if (y_center + (tooltip_box.height / 2) >= data_layer_height){ // Too close to the bottom, pull it up\n top = page_origin.y + y_center + arrow_width + border_radius - tooltip_box.height;\n arrow_top = tooltip_box.height - (2 * arrow_width) - border_radius;\n } else { // vertically centered\n top = page_origin.y + y_center - (tooltip_box.height / 2);\n arrow_top = (tooltip_box.height / 2) - arrow_width;\n }\n }\n // Apply positions to the main div\n tooltip.selector.style(\"left\", left + \"px\").style(\"top\", top + \"px\");\n // Create / update position on arrow connecting tooltip to data\n if (!tooltip.arrow){\n tooltip.arrow = tooltip.selector.append(\"div\").style(\"position\", \"absolute\");\n }\n tooltip.arrow\n .attr(\"class\", \"lz-data_layer-tooltip-arrow_\" + arrow_type)\n .style(\"left\", arrow_left + \"px\")\n .style(\"top\", arrow_top + \"px\");\n };\n\n // Function to flip labels from being anchored at the start of the text to the end\n // Both to keep labels from running outside the data layer and also as a first\n // pass on recursive separation\n this.flip_labels = function(){\n var data_layer = this;\n var point_size = data_layer.resolveScalableParameter(data_layer.layout.point_size, {});\n var spacing = data_layer.layout.label.spacing;\n var handle_lines = Boolean(data_layer.layout.label.lines);\n var min_x = 2 * spacing;\n var max_x = data_layer.parent.layout.width - data_layer.parent.layout.margin.left - data_layer.parent.layout.margin.right - (2 * spacing);\n var flip = function(dn, dnl){\n var dnx = +dn.attr(\"x\");\n var text_swing = (2 * spacing) + (2 * Math.sqrt(point_size));\n if (handle_lines){\n var dnlx2 = +dnl.attr(\"x2\");\n var line_swing = spacing + (2 * Math.sqrt(point_size));\n }\n if (dn.style(\"text-anchor\") === \"start\"){\n dn.style(\"text-anchor\", \"end\");\n dn.attr(\"x\", dnx - text_swing);\n if (handle_lines){ dnl.attr(\"x2\", dnlx2 - line_swing); }\n } else {\n dn.style(\"text-anchor\", \"start\");\n dn.attr(\"x\", dnx + text_swing);\n if (handle_lines){ dnl.attr(\"x2\", dnlx2 + line_swing); }\n }\n };\n // Flip any going over the right edge from the right side to the left side\n // (all labels start on the right side)\n data_layer.label_texts.each(function (d, i) {\n var a = this;\n var da = d3.select(a);\n var dax = +da.attr(\"x\");\n var abound = da.node().getBoundingClientRect();\n if (dax + abound.width + spacing > max_x){\n var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null;\n flip(da, dal);\n }\n });\n // Second pass to flip any others that haven't flipped yet if they collide with another label\n data_layer.label_texts.each(function (d, i) {\n var a = this;\n var da = d3.select(a);\n if (da.style(\"text-anchor\") === \"end\") return;\n var dax = +da.attr(\"x\");\n var abound = da.node().getBoundingClientRect();\n var dal = handle_lines ? d3.select(data_layer.label_lines[0][i]) : null;\n data_layer.label_texts.each(function () {\n var b = this;\n var db = d3.select(b);\n var bbound = db.node().getBoundingClientRect();\n var collision = abound.left < bbound.left + bbound.width + (2*spacing) &&\n abound.left + abound.width + (2*spacing) > bbound.left &&\n abound.top < bbound.top + bbound.height + (2*spacing) &&\n abound.height + abound.top + (2*spacing) > bbound.top;\n if (collision){\n flip(da, dal);\n // Double check that this flip didn't push the label past min_x. If it did, immediately flip back.\n dax = +da.attr(\"x\");\n if (dax - abound.width - spacing < min_x){\n flip(da, dal);\n }\n }\n return;\n });\n });\n };\n\n // Recursive function to space labels apart immediately after initial render\n // Adapted from thudfactor's fiddle here: https://jsfiddle.net/thudfactor/HdwTH/\n // TODO: Make labels also aware of data elements\n this.separate_labels = function(){\n this.seperate_iterations++;\n var data_layer = this;\n var alpha = 0.5;\n var spacing = this.layout.label.spacing;\n var again = false;\n data_layer.label_texts.each(function () {\n var a = this;\n var da = d3.select(a);\n var y1 = da.attr(\"y\");\n data_layer.label_texts.each(function () {\n var b = this;\n // a & b are the same element and don't collide.\n if (a === b) return;\n var db = d3.select(b);\n // a & b are on opposite sides of the chart and\n // don't collide\n if (da.attr(\"text-anchor\") !== db.attr(\"text-anchor\")) return;\n // Determine if the bounding rects for the two text elements collide\n var abound = da.node().getBoundingClientRect();\n var bbound = db.node().getBoundingClientRect();\n var collision = abound.left < bbound.left + bbound.width + (2*spacing) &&\n abound.left + abound.width + (2*spacing) > bbound.left &&\n abound.top < bbound.top + bbound.height + (2*spacing) &&\n abound.height + abound.top + (2*spacing) > bbound.top;\n if (!collision) return;\n again = true;\n // If the labels collide, we'll push each\n // of the two labels up and down a little bit.\n var y2 = db.attr(\"y\");\n var sign = abound.top < bbound.top ? 1 : -1;\n var adjust = sign * alpha;\n var new_a_y = +y1 - adjust;\n var new_b_y = +y2 + adjust;\n // Keep new values from extending outside the data layer\n var min_y = 2 * spacing;\n var max_y = data_layer.parent.layout.height - data_layer.parent.layout.margin.top - data_layer.parent.layout.margin.bottom - (2 * spacing);\n var delta;\n if (new_a_y - (abound.height/2) < min_y){\n delta = +y1 - new_a_y;\n new_a_y = +y1;\n new_b_y += delta;\n } else if (new_b_y - (bbound.height/2) < min_y){\n delta = +y2 - new_b_y;\n new_b_y = +y2;\n new_a_y += delta;\n }\n if (new_a_y + (abound.height/2) > max_y){\n delta = new_a_y - +y1;\n new_a_y = +y1;\n new_b_y -= delta;\n } else if (new_b_y + (bbound.height/2) > max_y){\n delta = new_b_y - +y2;\n new_b_y = +y2;\n new_a_y -= delta;\n }\n da.attr(\"y\",new_a_y);\n db.attr(\"y\",new_b_y);\n });\n });\n if (again) {\n // Adjust lines to follow the labels\n if (data_layer.layout.label.lines){\n var label_elements = data_layer.label_texts[0];\n data_layer.label_lines.attr(\"y2\",function(d,i) {\n var label_line = d3.select(label_elements[i]);\n return label_line.attr(\"y\");\n });\n }\n // After ~150 iterations we're probably beyond diminising returns, so stop recursing\n if (this.seperate_iterations < 150){\n setTimeout(function(){\n this.separate_labels();\n }.bind(this), 1);\n }\n }\n };\n\n // Implement the main render function\n this.render = function(){\n\n var data_layer = this;\n var x_scale = \"x_scale\";\n var y_scale = \"y\"+this.layout.y_axis.axis+\"_scale\";\n\n // Generate labels first (if defined)\n if (this.layout.label){\n // Apply filters to generate a filtered data set\n var filtered_data = this.data.filter(function(d){\n if (!data_layer.layout.label.filters){\n return true;\n } else {\n // Start by assuming a match, run through all filters to test if not a match on any one\n var match = true;\n data_layer.layout.label.filters.forEach(function(filter){\n var field_value = (new LocusZoom.Data.Field(filter.field)).resolve(d);\n if (isNaN(field_value)){\n match = false;\n } else {\n switch (filter.operator){\n case \"<\":\n if (!(field_value < filter.value)){ match = false; }\n break;\n case \"<=\":\n if (!(field_value <= filter.value)){ match = false; }\n break;\n case \">\":\n if (!(field_value > filter.value)){ match = false; }\n break;\n case \">=\":\n if (!(field_value >= filter.value)){ match = false; }\n break;\n case \"=\":\n if (!(field_value === filter.value)){ match = false; }\n break;\n default:\n // If we got here the operator is not valid, so the filter should fail\n match = false;\n break;\n }\n }\n });\n return match;\n }\n });\n // Render label groups\n var self = this;\n this.label_groups = this.svg.group\n .selectAll(\"g.lz-data_layer-\" + this.layout.type + \"-label\")\n .data(filtered_data, function(d){ return d[self.layout.id_field] + \"_label\"; });\n this.label_groups.enter()\n .append(\"g\")\n .attr(\"class\", \"lz-data_layer-\"+ this.layout.type + \"-label\");\n // Render label texts\n if (this.label_texts){ this.label_texts.remove(); }\n this.label_texts = this.label_groups.append(\"text\")\n .attr(\"class\", \"lz-data_layer-\" + this.layout.type + \"-label\");\n this.label_texts\n .text(function(d){\n return LocusZoom.parseFields(d, data_layer.layout.label.text || \"\");\n })\n .style(data_layer.layout.label.style || {})\n .attr({\n \"x\": function(d){\n var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field])\n + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d))\n + data_layer.layout.label.spacing;\n if (isNaN(x)){ x = -1000; }\n return x;\n },\n \"y\": function(d){\n var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]);\n if (isNaN(y)){ y = -1000; }\n return y;\n },\n \"text-anchor\": function(){\n return \"start\";\n }\n });\n // Render label lines\n if (data_layer.layout.label.lines){\n if (this.label_lines){ this.label_lines.remove(); }\n this.label_lines = this.label_groups.append(\"line\")\n .attr(\"class\", \"lz-data_layer-\" + this.layout.type + \"-label\");\n this.label_lines\n .style(data_layer.layout.label.lines.style || {})\n .attr({\n \"x1\": function(d){\n var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field]);\n if (isNaN(x)){ x = -1000; }\n return x;\n },\n \"y1\": function(d){\n var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]);\n if (isNaN(y)){ y = -1000; }\n return y;\n },\n \"x2\": function(d){\n var x = data_layer.parent[x_scale](d[data_layer.layout.x_axis.field])\n + Math.sqrt(data_layer.resolveScalableParameter(data_layer.layout.point_size, d))\n + (data_layer.layout.label.spacing/2);\n if (isNaN(x)){ x = -1000; }\n return x;\n },\n \"y2\": function(d){\n var y = data_layer.parent[y_scale](d[data_layer.layout.y_axis.field]);\n if (isNaN(y)){ y = -1000; }\n return y;\n }\n });\n }\n // Remove labels when they're no longer in the filtered data set\n this.label_groups.exit().remove();\n }\n \n // Generate main scatter data elements\n var selection = this.svg.group\n .selectAll(\"path.lz-data_layer-\" + this.layout.type)\n .data(this.data, function(d){ return d[this.layout.id_field]; }.bind(this));\n\n // Create elements, apply class, ID, and initial position\n var initial_y = isNaN(this.parent.layout.height) ? 0 : this.parent.layout.height;\n selection.enter()\n .append(\"path\")\n .attr(\"class\", \"lz-data_layer-\" + this.layout.type)\n .attr(\"id\", function(d){ return this.getElementId(d); }.bind(this))\n .attr(\"transform\", \"translate(0,\" + initial_y + \")\");\n\n // Generate new values (or functions for them) for position, color, size, and shape\n var transform = function(d) {\n var x = this.parent[x_scale](d[this.layout.x_axis.field]);\n var y = this.parent[y_scale](d[this.layout.y_axis.field]);\n if (isNaN(x)){ x = -1000; }\n if (isNaN(y)){ y = -1000; }\n return \"translate(\" + x + \",\" + y + \")\";\n }.bind(this);\n\n var fill = function(d){ return this.resolveScalableParameter(this.layout.color, d); }.bind(this);\n var fill_opacity = function(d){ return this.resolveScalableParameter(this.layout.fill_opacity, d); }.bind(this);\n\n var shape = d3.svg.symbol()\n .size(function(d){ return this.resolveScalableParameter(this.layout.point_size, d); }.bind(this))\n .type(function(d){ return this.resolveScalableParameter(this.layout.point_shape, d); }.bind(this));\n\n // Apply position and color, using a transition if necessary\n\n if (this.canTransition()){\n selection\n .transition()\n .duration(this.layout.transition.duration || 0)\n .ease(this.layout.transition.ease || \"cubic-in-out\")\n .attr(\"transform\", transform)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity)\n .attr(\"d\", shape);\n } else {\n selection\n .attr(\"transform\", transform)\n .attr(\"fill\", fill)\n .attr(\"fill-opacity\", fill_opacity)\n .attr(\"d\", shape);\n }\n\n // Remove old elements as needed\n selection.exit().remove();\n\n // Apply default event emitters to selection\n selection.on(\"click.event_emitter\", function(element){\n this.parent.emit(\"element_clicked\", element);\n this.parent_plot.emit(\"element_clicked\", element);\n }.bind(this));\n \n // Apply mouse behaviors\n this.applyBehaviors(selection);\n \n // Apply method to keep labels from overlapping each other\n if (this.layout.label){\n this.flip_labels();\n this.seperate_iterations = 0;\n this.separate_labels();\n // Apply default event emitters to selection\n this.label_texts.on(\"click.event_emitter\", function(element){\n this.parent.emit(\"element_clicked\", element);\n this.parent_plot.emit(\"element_clicked\", element);\n }.bind(this));\n // Extend mouse behaviors to labels\n this.applyBehaviors(this.label_texts);\n }\n \n };\n\n // Method to set a passed element as the LD reference in the plot-level state\n this.makeLDReference = function(element){\n var ref = null;\n if (typeof element == \"undefined\"){\n throw(\"makeLDReference requires one argument of any type\");\n } else if (typeof element == \"object\"){\n if (this.layout.id_field && typeof element[this.layout.id_field] != \"undefined\"){\n ref = element[this.layout.id_field].toString();\n } else if (typeof element[\"id\"] != \"undefined\"){\n ref = element[\"id\"].toString();\n } else {\n ref = element.toString();\n }\n } else {\n ref = element.toString();\n }\n this.parent_plot.applyState({ ldrefvar: ref });\n };\n \n return this;\n\n});\n\n/**\n * A scatter plot in which the x-axis represents categories, rather than individual positions.\n * For example, this can be used by PheWAS plots to show related groups. This plot allows the categories to be\n * determined dynamically when data is first loaded.\n *\n * @class LocusZoom.DataLayers.category_scatter\n * @augments LocusZoom.DataLayers.scatter\n */\nLocusZoom.DataLayers.extend(\"scatter\", \"category_scatter\", {\n /**\n * This plot layer makes certain assumptions about the data passed in. Transform the raw array of records from\n * the datasource to prepare it for plotting, as follows:\n * 1. The scatter plot assumes that all records are given in sequence (pre-grouped by `category_field`)\n * 2. It assumes that all records have an x coordinate for individual plotting\n * @private\n */\n _prepareData: function() {\n var xField = this.layout.x_axis.field || \"x\";\n // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color\n var category_field = this.layout.x_axis.category_field;\n if (!category_field) {\n throw \"Layout for \" + this.layout.id + \" must specify category_field\";\n }\n // Sort the data so that things in the same category are adjacent (case-insensitive by specified field)\n var sourceData = this.data\n .sort(function(a, b) {\n var ak = a[category_field];\n var bk = b[category_field];\n var av = ak.toString ? ak.toString().toLowerCase() : ak;\n var bv = bk.toString ? bk.toString().toLowerCase() : bk;\n return (av === bv) ? 0 : (av < bv ? -1 : 1);});\n sourceData.forEach(function(d, i){\n // Implementation detail: Scatter plot requires specifying an x-axis value, and most datasources do not\n // specify plotting positions. If a point is missing this field, fill in a synthetic value.\n d[xField] = d[xField] || i;\n });\n return sourceData;\n },\n\n /**\n * Identify the unique categories on the plot, and update the layout with an appropriate color scheme.\n * Also identify the min and max x value associated with the category, which will be used to generate ticks\n * @private\n * @returns {Object.} Series of entries used to build category name ticks {category_name: [min_x, max_x]}\n */\n _generateCategoryBounds: function() {\n // TODO: API may return null values in category_field; should we add placeholder category label?\n // The (namespaced) field from `this.data` that will be used to assign datapoints to a given category & color\n var category_field = this.layout.x_axis.category_field;\n var xField = this.layout.x_axis.field || \"x\";\n var uniqueCategories = {};\n this.data.forEach(function(item) {\n var category = item[category_field];\n var x = item[xField];\n var bounds = uniqueCategories[category] || [x, x];\n uniqueCategories[category] = [Math.min(bounds[0], x), Math.max(bounds[1], x)];\n });\n\n var categoryNames = Object.keys(uniqueCategories);\n this._setDynamicColorScheme(categoryNames);\n\n return uniqueCategories;\n },\n\n /**\n * Automatically define a color scheme for the layer based on data returned from the server.\n * If part of the color scheme has been specified, it will fill in remaining missing information.\n *\n * There are three scenarios:\n * 1. The layout does not specify either category names or (color) values. Dynamically build both based on\n * the data and update the layout.\n * 2. The layout specifies colors, but not categories. Use that exact color information provided, and dynamically\n * determine what categories are present in the data. (cycle through the available colors, reusing if there\n * are a lot of categories)\n * 3. The layout specifies exactly what colors and categories to use (and they match the data!). This is useful to\n * specify an explicit mapping between color scheme and category names, when you want to be sure that the\n * plot matches a standard color scheme.\n * (If the layout specifies categories that do not match the data, the user specified categories will be ignored)\n *\n * This method will only act if the layout defines a `categorical_bin` scale function for coloring. It may be\n * overridden in a subclass to suit other types of coloring methods.\n *\n * @param {String[]} categoryNames\n * @private\n */\n _setDynamicColorScheme: function(categoryNames) {\n var colorParams = this.layout.color.parameters;\n var baseParams = this._base_layout.color.parameters;\n\n // If the layout does not use a supported coloring scheme, or is already complete, this method should do nothing\n if (this.layout.color.scale_function !== \"categorical_bin\") {\n throw \"This layer requires that coloring be specified as a `categorical_bin`\";\n }\n\n if (baseParams.categories.length && baseParams.values.length) {\n // If there are preset category/color combos, make sure that they apply to the actual dataset\n var parameters_categories_hash = {};\n baseParams.categories.forEach(function (category) { parameters_categories_hash[category] = 1; });\n if (categoryNames.every(function (name) { return parameters_categories_hash.hasOwnProperty(name); })) {\n // The layout doesn't have to specify categories in order, but make sure they are all there\n colorParams.categories = baseParams.categories;\n } else {\n colorParams.categories = categoryNames;\n }\n } else {\n colorParams.categories = categoryNames;\n }\n // Prefer user-specified colors if provided. Make sure that there are enough colors for all the categories.\n var colors;\n if (baseParams.values.length) {\n colors = baseParams.values;\n } else {\n var color_scale = categoryNames.length <= 10 ? d3.scale.category10 : d3.scale.category20;\n colors = color_scale().range();\n }\n while (colors.length < categoryNames.length) { colors = colors.concat(colors); }\n colors = colors.slice(0, categoryNames.length); // List of hex values, should be of same length as categories array\n colorParams.values = colors;\n },\n\n /**\n *\n * @param dimension\n * @param {Object} [config] Parameters that customize how ticks are calculated (not style)\n * @param {('left'|'center'|'right')} [config.position='left'] Align ticks with the center or edge of category\n * @returns {Array}\n */\n getTicks: function(dimension, config) { // Overrides parent method\n if ([\"x\", \"y\"].indexOf(dimension) === -1) {\n throw \"Invalid dimension identifier\";\n }\n var position = config.position || \"left\";\n if ([\"left\", \"center\", \"right\"].indexOf(position) === -1) {\n throw \"Invalid tick position\";\n }\n\n var categoryBounds = this._categories;\n if (!categoryBounds || !Object.keys(categoryBounds).length) {\n return [];\n }\n\n if (dimension === \"y\") {\n return [];\n }\n\n if (dimension === \"x\") {\n // If colors have been defined by this layer, use them to make tick colors match scatterplot point colors\n var knownCategories = this.layout.color.parameters.categories || [];\n var knownColors = this.layout.color.parameters.values || [];\n\n return Object.keys(categoryBounds).map(function (category, index) {\n var bounds = categoryBounds[category];\n var xPos;\n\n switch(position) {\n case \"left\":\n xPos = bounds[0];\n break;\n case \"center\":\n // Center tick under one or many elements as appropriate\n var diff = bounds[1] - bounds[0];\n xPos = bounds[0] + (diff !== 0 ? diff : bounds[0]) / 2;\n break;\n case \"right\":\n xPos = bounds[1];\n break;\n }\n return {\n x: xPos,\n text: category,\n style: {\n \"fill\": knownColors[knownCategories.indexOf(category)] || \"#000000\"\n }\n };\n });\n }\n },\n\n applyCustomDataMethods: function() {\n this.data = this._prepareData();\n /**\n * Define category names and extents (boundaries) for plotting. TODO: properties in constructor\n * @member {Object.} Category names and extents, in the form {category_name: [min_x, max_x]}\n */\n this._categories = this._generateCategoryBounds();\n return this;\n }\n});\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n *\n * LocusZoom has various singleton objects that are used for registering functions or classes.\n * These objects provide safe, standard methods to redefine or delete existing functions/classes\n * as well as define new custom functions/classes to be used in a plot.\n *\n * @namespace Singletons\n */\n\n\n/*\n * The Collection of \"Known\" Data Sources. This registry is used internally by the `DataSources` class\n * @class\n * @static\n */\nLocusZoom.KnownDataSources = (function() {\n /** @lends LocusZoom.KnownDataSources */\n var obj = {};\n /* @member {function[]} */\n var sources = [];\n\n var findSourceByName = function(x) {\n for(var i=0; i 1) {\n return function(x) {\n var val = x;\n for(var i = 0; i 1){\n log = Math.ceil(Math.log(x) / Math.LN10);\n } else {\n log = Math.floor(Math.log(x) / Math.LN10);\n }\n if (Math.abs(log) <= 3){\n return x.toFixed(3);\n } else {\n return x.toExponential(2).replace(\"+\", \"\").replace(\"e\", \" × 10^\");\n }\n});\n\n/**\n * URL-encode the provided text, eg for constructing hyperlinks\n * @function urlencode\n * @param {String} str\n */\nLocusZoom.TransformationFunctions.add(\"urlencode\", function(str) {\n return encodeURIComponent(str);\n});\n\n/**\n * HTML-escape user entered values for use in constructed HTML fragments\n *\n * For example, this filter can be used on tooltips with custom HTML display\n * @function htmlescape\n * @param {String} str HTML-escape the provided value\n */\nLocusZoom.TransformationFunctions.add(\"htmlescape\", function(str) {\n if ( !str ) {\n return \"\";\n }\n str = str + \"\";\n\n return str.replace( /['\"<>&`]/g, function( s ) {\n switch ( s ) {\n case \"'\":\n return \"'\";\n case \"\\\"\":\n return \""\";\n case \"<\":\n return \"<\";\n case \">\":\n return \">\";\n case \"&\":\n return \"&\";\n case \"`\":\n return \"`\";\n }\n });\n});\n\n/**\n * Singleton for accessing/storing functions that will convert arbitrary data points to values in a given scale\n * Useful for anything that needs to scale discretely with data (e.g. color, point size, etc.)\n *\n * A Scale Function can be thought of as a modifier to a layout directive that adds extra logic to how a piece of data\n * can be resolved to a value.\n *\n * All scale functions must accept an object of parameters and a value to process.\n * @class\n * @static\n */\nLocusZoom.ScaleFunctions = (function() {\n /** @lends LocusZoom.ScaleFunctions */\n var obj = {};\n var functions = {};\n\n /**\n * Find a scale function and return it. If parameters and values are passed, calls the function directly; otherwise\n * returns a callable.\n * @param {String} name\n * @param {Object} [parameters] Configuration parameters specific to the specified scale function\n * @param {*} [value] The value to operate on\n * @returns {*}\n */\n obj.get = function(name, parameters, value) {\n if (!name) {\n return null;\n } else if (functions[name]) {\n if (typeof parameters === \"undefined\" && typeof value === \"undefined\"){\n return functions[name];\n } else {\n return functions[name](parameters, value);\n }\n } else {\n throw(\"scale function [\" + name + \"] not found\");\n }\n };\n\n /**\n * @protected\n * @param {String} name The name of the function to set/unset\n * @param {Function} [fn] The function to register. If blank, removes this function name from the registry.\n */\n obj.set = function(name, fn) {\n if (fn) {\n functions[name] = fn;\n } else {\n delete functions[name];\n }\n };\n\n /**\n * Add a new scale function to the registry\n * @param {String} name The name of the scale function\n * @param {function} fn A scale function that accepts two parameters: an object of configuration and a value\n */\n obj.add = function(name, fn) {\n if (functions[name]) {\n throw(\"scale function already exists with name: \" + name);\n } else {\n obj.set(name, fn);\n }\n };\n\n /**\n * List the names of all registered scale functions\n * @returns {String[]}\n */\n obj.list = function() {\n return Object.keys(functions);\n };\n\n return obj;\n})();\n\n/**\n * Basic conditional function to evaluate the value of the input field and return based on equality.\n * @param {Object} parameters\n * @param {*} parameters.field_value The value against which to test the input value.\n * @param {*} parameters.then The value to return if the input value matches the field value\n * @param {*} parameters.else The value to return if the input value does not match the field value. Optional. If not\n * defined this scale function will return null (or value of null_value parameter, if defined) when input value fails\n * to match field_value.\n * @param {*} input value\n */\nLocusZoom.ScaleFunctions.add(\"if\", function(parameters, input){\n if (typeof input == \"undefined\" || parameters.field_value !== input){\n if (typeof parameters.else != \"undefined\"){\n return parameters.else;\n } else {\n return null;\n }\n } else {\n return parameters.then;\n }\n});\n\n/**\n * Function to sort numerical values into bins based on numerical break points. Will only operate on numbers and\n * return null (or value of null_value parameter, if defined) if provided a non-numeric input value. Parameters:\n * @function numerical_bin\n * @param {Object} parameters\n * @param {Number[]} parameters.breaks Array of numerical break points against which to evaluate the input value.\n * Must be of equal length to values parameter. If the input value is greater than or equal to break n and less than\n * or equal to break n+1 (or break n+1 doesn't exist) then returned value is the nth entry in the values parameter.\n * @param {Array} parameters.values Array of values to return given evaluations against break points. Must be of\n * equal length to breaks parameter. Each entry n represents the value to return if the input value is greater than\n * or equal to break n and less than or equal to break n+1 (or break n+1 doesn't exist).\n * @param {*} null_value\n * @param {*} input value\n * @returns\n */\nLocusZoom.ScaleFunctions.add(\"numerical_bin\", function(parameters, input){\n var breaks = parameters.breaks || [];\n var values = parameters.values || [];\n if (typeof input == \"undefined\" || input === null || isNaN(+input)){\n return (parameters.null_value ? parameters.null_value : null);\n }\n var threshold = breaks.reduce(function(prev, curr){\n if (+input < prev || (+input >= prev && +input < curr)){\n return prev;\n } else {\n return curr;\n }\n });\n return values[breaks.indexOf(threshold)];\n});\n\n/**\n * Function to sort values of any type into bins based on direct equality testing with a list of categories.\n * Will return null if provided an input value that does not match to a listed category.\n * @function categorical_bin\n * @param {Object} parameters\n * @param {Array} parameters.categories Array of values against which to evaluate the input value. Must be of equal\n * length to values parameter. If the input value is equal to category n then returned value is the nth entry in the\n * values parameter.\n * @param {Array} parameters.values Array of values to return given evaluations against categories. Must be of equal\n * length to categories parameter. Each entry n represents the value to return if the input value is equal to the nth\n * value in the categories parameter.\n * @param {*} parameters.null_value Value to return if the input value fails to match to any categories. Optional.\n */\nLocusZoom.ScaleFunctions.add(\"categorical_bin\", function(parameters, value){\n if (typeof value == \"undefined\" || parameters.categories.indexOf(value) === -1){\n return (parameters.null_value ? parameters.null_value : null); \n } else {\n return parameters.values[parameters.categories.indexOf(value)];\n }\n});\n\n/**\n * Function for continuous interpolation of numerical values along a gradient with arbitrarily many break points.\n * @function interpolate\n * @parameters {Object} parameters\n * @parameters {Number[]} parameters.breaks Array of numerical break points against which to evaluate the input value.\n * Must be of equal length to values parameter and contain at least two elements. Input value will be evaluated for\n * relative position between two break points n and n+1 and the returned value will be interpolated at a relative\n * position between values n and n+1.\n * @parameters {*[]} parameters.values Array of values to interpolate and return given evaluations against break\n * points. Must be of equal length to breaks parameter and contain at least two elements. Each entry n represents\n * the value to return if the input value matches the nth entry in breaks exactly. Note that this scale function\n * uses d3.interpolate to provide for effective interpolation of many different value types, including numbers,\n * colors, shapes, etc.\n * @parameters {*} parameters.null_value\n */\nLocusZoom.ScaleFunctions.add(\"interpolate\", function(parameters, input){\n var breaks = parameters.breaks || [];\n var values = parameters.values || [];\n var nullval = (parameters.null_value ? parameters.null_value : null);\n if (breaks.length < 2 || breaks.length !== values.length){ return nullval; }\n if (typeof input == \"undefined\" || input === null || isNaN(+input)){ return nullval; }\n if (+input <= parameters.breaks[0]){\n return values[0];\n } else if (+input >= parameters.breaks[parameters.breaks.length-1]){\n return values[breaks.length-1];\n } else {\n var upper_idx = null;\n breaks.forEach(function(brk, idx){\n if (!idx){ return; }\n if (breaks[idx-1] <= +input && breaks[idx] >= +input){ upper_idx = idx; }\n });\n if (upper_idx === null){ return nullval; }\n var normalized_input = (+input - breaks[upper_idx-1]) / (breaks[upper_idx] - breaks[upper_idx-1]);\n if (!isFinite(normalized_input)){ return nullval; }\n return d3.interpolate(values[upper_idx-1], values[upper_idx])(normalized_input);\n }\n});\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * A Dashboard is an HTML element used for presenting arbitrary user interface components. Dashboards are anchored\n * to either the entire Plot or to individual Panels.\n *\n * Each dashboard is an HTML-based (read: not SVG) collection of components used to display information or provide\n * user interface. Dashboards can exist on entire plots, where their visibility is permanent and vertically adjacent\n * to the plot, or on individual panels, where their visibility is tied to a behavior (e.g. a mouseover) and is as\n * an overlay.\n * @class\n */\nLocusZoom.Dashboard = function(parent){\n // parent must be a locuszoom plot or panel\n if (!(parent instanceof LocusZoom.Plot) && !(parent instanceof LocusZoom.Panel)){\n throw \"Unable to create dashboard, parent must be a locuszoom plot or panel\";\n }\n /** @member {LocusZoom.Plot|LocusZoom.Panel} */\n this.parent = parent;\n /** @member {String} */\n this.id = this.parent.getBaseId() + \".dashboard\";\n /** @member {('plot'|'panel')} */\n this.type = (this.parent instanceof LocusZoom.Plot) ? \"plot\" : \"panel\";\n /** @member {LocusZoom.Plot} */\n this.parent_plot = this.type === \"plot\" ? this.parent : this.parent.parent;\n\n /** @member {d3.selection} */\n this.selector = null;\n /** @member {LocusZoom.Dashboard.Component[]} */\n this.components = [];\n /**\n * The timer identifier as returned by setTimeout\n * @member {Number}\n */\n this.hide_timeout = null;\n /**\n * Whether to hide the dashboard. Can be overridden by a child component. Check via `shouldPersist`\n * @protected\n * @member {Boolean}\n */\n this.persist = false;\n\n // TODO: Return value from constructor function?\n return this.initialize();\n};\n\n/**\n * Prepare the dashboard for first use: generate all component instances for this dashboard, based on the provided\n * layout of the parent. Connects event listeners and shows/hides as appropriate.\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.prototype.initialize = function() {\n // Parse layout to generate component instances\n if (Array.isArray(this.parent.layout.dashboard.components)){\n this.parent.layout.dashboard.components.forEach(function(layout){\n try {\n var component = LocusZoom.Dashboard.Components.get(layout.type, layout, this);\n this.components.push(component);\n } catch (e) {\n console.warn(e);\n }\n }.bind(this));\n }\n\n // Add mouseover event handlers to show/hide panel dashboard\n if (this.type === \"panel\"){\n d3.select(this.parent.parent.svg.node().parentNode).on(\"mouseover.\" + this.id, function(){\n clearTimeout(this.hide_timeout);\n if (!this.selector || this.selector.style(\"visibility\") === \"hidden\"){ this.show(); }\n }.bind(this));\n d3.select(this.parent.parent.svg.node().parentNode).on(\"mouseout.\" + this.id, function(){\n clearTimeout(this.hide_timeout);\n this.hide_timeout = setTimeout(function(){ this.hide(); }.bind(this), 300);\n }.bind(this));\n }\n\n return this;\n\n};\n\n/**\n * Whether to persist the dashboard. Returns true if at least one component should persist, or if the panel is engaged\n * in an active drag event.\n * @returns {boolean}\n */\nLocusZoom.Dashboard.prototype.shouldPersist = function(){\n if (this.persist){ return true; }\n var persist = false;\n // Persist if at least one component should also persist\n this.components.forEach(function(component){\n persist = persist || component.shouldPersist();\n });\n // Persist if in a parent drag event\n persist = persist || (this.parent_plot.panel_boundaries.dragging || this.parent_plot.interaction.dragging);\n return !!persist;\n};\n\n/**\n * Make the dashboard appear. If it doesn't exist yet create it, including creating/positioning all components within,\n * and make sure it is set to be visible.\n */\nLocusZoom.Dashboard.prototype.show = function(){\n if (!this.selector){\n switch (this.type){\n case \"plot\":\n this.selector = d3.select(this.parent.svg.node().parentNode)\n .insert(\"div\",\":first-child\");\n break;\n case \"panel\":\n this.selector = d3.select(this.parent.parent.svg.node().parentNode)\n .insert(\"div\", \".lz-data_layer-tooltip, .lz-dashboard-menu, .lz-curtain\").classed(\"lz-panel-dashboard\", true);\n break;\n }\n this.selector.classed(\"lz-dashboard\", true).classed(\"lz-\"+this.type+\"-dashboard\", true).attr(\"id\", this.id);\n }\n this.components.forEach(function(component){ component.show(); });\n this.selector.style({ visibility: \"visible\" });\n return this.update();\n};\n\n/**\n * Update the dashboard and rerender all child components. This can be called whenever plot state changes.\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.prototype.update = function(){\n if (!this.selector){ return this; }\n this.components.forEach(function(component){ component.update(); });\n return this.position();\n};\n\n/**\n * Position the dashboard (and child components) within the panel\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.prototype.position = function(){\n if (!this.selector){ return this; }\n // Position the dashboard itself (panel only)\n if (this.type === \"panel\"){\n var page_origin = this.parent.getPageOrigin();\n var top = (page_origin.y + 3.5).toString() + \"px\";\n var left = page_origin.x.toString() + \"px\";\n var width = (this.parent.layout.width - 4).toString() + \"px\";\n this.selector.style({ position: \"absolute\", top: top, left: left, width: width });\n }\n // Recursively position components\n this.components.forEach(function(component){ component.position(); });\n return this;\n};\n\n/**\n * Hide the dashboard (make invisible but do not destroy). Will do nothing if `shouldPersist` returns true.\n *\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.prototype.hide = function(){\n if (!this.selector || this.shouldPersist()){ return this; }\n this.components.forEach(function(component){ component.hide(); });\n this.selector.style({ visibility: \"hidden\" });\n return this;\n};\n\n/**\n * Completely remove dashboard and all child components. (may be overridden by persistence settings)\n * @param {Boolean} [force=false] If true, will ignore persistence settings and always destroy the dashboard\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.prototype.destroy = function(force){\n if (typeof force == \"undefined\"){ force = false; }\n if (!this.selector){ return this; }\n if (this.shouldPersist() && !force){ return this; }\n this.components.forEach(function(component){ component.destroy(true); });\n this.components = [];\n this.selector.remove();\n this.selector = null;\n return this;\n};\n\n/**\n *\n * A dashboard component is an empty div rendered on a dashboard that can display custom\n * html of user interface elements. LocusZoom.Dashboard.Components is a singleton used to\n * define and manage an extendable collection of dashboard components.\n * (e.g. by LocusZoom.Dashboard.Components.add())\n * @class\n * @param {Object} layout A JSON-serializable object of layout configuration parameters\n * @param {('left'|'right')} [layout.position='left'] Whether to float the component left or right.\n * @param {('start'|'middle'|'end')} [layout.group_position] Buttons can optionally be gathered into a visually\n * distinctive group whose elements are closer together. If a button is identified as the start or end of a group,\n * it will be drawn with rounded corners and an extra margin of spacing from any button not part of the group.\n * For example, the region_nav_plot dashboard is a defined as a group.\n * @param {('gray'|'red'|'orange'|'yellow'|'green'|'blue'|'purple'} [layout.color='gray'] Color scheme for the\n * component. Applies to buttons and menus.\n * @param {LocusZoom.Dashboard} parent The dashboard that contains this component\n*/\nLocusZoom.Dashboard.Component = function(layout, parent) {\n /** @member {Object} */\n this.layout = layout || {};\n if (!this.layout.color){ this.layout.color = \"gray\"; }\n\n /** @member {LocusZoom.Dashboard|*} */\n this.parent = parent || null;\n /**\n * Some dashboards are attached to a panel, rather than directly to a plot\n * @member {LocusZoom.Panel|null}\n */\n this.parent_panel = null;\n /** @member {LocusZoom.Plot} */\n this.parent_plot = null;\n /**\n * This is a reference to either the panel or the plot, depending on what the dashboard is\n * tied to. Useful when absolutely positioning dashboard components relative to their SVG anchor.\n * @member {LocusZoom.Plot|LocusZoom.Panel}\n */\n this.parent_svg = null;\n if (this.parent instanceof LocusZoom.Dashboard){\n // TODO: when is the immediate parent *not* a dashboard?\n if (this.parent.type === \"panel\"){\n this.parent_panel = this.parent.parent;\n this.parent_plot = this.parent.parent.parent;\n this.parent_svg = this.parent_panel;\n } else {\n this.parent_plot = this.parent.parent;\n this.parent_svg = this.parent_plot;\n }\n }\n /** @member {d3.selection} */\n this.selector = null;\n /**\n * If this is an interactive component, it will contain a button or menu instance that handles the interactivity.\n * There is a 1-to-1 relationship of dashboard component to button\n * @member {null|LocusZoom.Dashboard.Component.Button}\n */\n this.button = null;\n /**\n * If any single component is marked persistent, it will bubble up to prevent automatic hide behavior on a\n * component's parent dashboard. Check via `shouldPersist`\n * @protected\n * @member {Boolean}\n */\n this.persist = false;\n if (!this.layout.position){ this.layout.position = \"left\"; }\n\n // TODO: Return value in constructor\n return this;\n};\n/**\n * Perform all rendering of component, including toggling visibility to true. Will initialize and create SVG element\n * if necessary, as well as updating with new data and performing layout actions.\n */\nLocusZoom.Dashboard.Component.prototype.show = function(){\n if (!this.parent || !this.parent.selector){ return; }\n if (!this.selector){\n var group_position = ([\"start\",\"middle\",\"end\"].indexOf(this.layout.group_position) !== -1 ? \" lz-dashboard-group-\" + this.layout.group_position : \"\");\n this.selector = this.parent.selector.append(\"div\")\n .attr(\"class\", \"lz-dashboard-\" + this.layout.position + group_position);\n if (this.layout.style){ this.selector.style(this.layout.style); }\n if (typeof this.initialize == \"function\"){ this.initialize(); }\n }\n if (this.button && this.button.status === \"highlighted\"){ this.button.menu.show(); }\n this.selector.style({ visibility: \"visible\" });\n this.update();\n return this.position();\n};\n/**\n * Update the dashboard component with any new data or plot state as appropriate. This method performs all\n * necessary rendering steps.\n */\nLocusZoom.Dashboard.Component.prototype.update = function(){ /* stub */ };\n/**\n * Place the component correctly in the plot\n * @returns {LocusZoom.Dashboard.Component}\n */\nLocusZoom.Dashboard.Component.prototype.position = function(){\n if (this.button){ this.button.menu.position(); }\n return this;\n};\n/**\n * Determine whether the component should persist (will bubble up to parent dashboard)\n * @returns {boolean}\n */\nLocusZoom.Dashboard.Component.prototype.shouldPersist = function(){\n if (this.persist){ return true; }\n if (this.button && this.button.persist){ return true; }\n return false;\n};\n/**\n * Toggle visibility to hidden, unless marked as persistent\n * @returns {LocusZoom.Dashboard.Component}\n */\nLocusZoom.Dashboard.Component.prototype.hide = function(){\n if (!this.selector || this.shouldPersist()){ return this; }\n if (this.button){ this.button.menu.hide(); }\n this.selector.style({ visibility: \"hidden\" });\n return this;\n};\n/**\n * Completely remove component and button. (may be overridden by persistence settings)\n * @param {Boolean} [force=false] If true, will ignore persistence settings and always destroy the dashboard\n * @returns {LocusZoom.Dashboard}\n */\nLocusZoom.Dashboard.Component.prototype.destroy = function(force){\n if (typeof force == \"undefined\"){ force = false; }\n if (!this.selector){ return this; }\n if (this.shouldPersist() && !force){ return this; }\n if (this.button && this.button.menu){ this.button.menu.destroy(); }\n this.selector.remove();\n this.selector = null;\n this.button = null;\n return this;\n};\n\n/**\n * Singleton registry of all known components\n * @class\n * @static\n */\nLocusZoom.Dashboard.Components = (function() {\n /** @lends LocusZoom.Dashboard.Components */\n var obj = {};\n var components = {};\n\n /**\n * Create a new component instance by name\n * @param {String} name The string identifier of the desired component\n * @param {Object} layout The layout to use to create the component\n * @param {LocusZoom.Dashboard} parent The containing dashboard to use when creating the component\n * @returns {LocusZoom.Dashboard.Component}\n */\n obj.get = function(name, layout, parent) {\n if (!name) {\n return null;\n } else if (components[name]) {\n if (typeof layout != \"object\"){\n throw(\"invalid layout argument for dashboard component [\" + name + \"]\");\n } else {\n return new components[name](layout, parent);\n }\n } else {\n throw(\"dashboard component [\" + name + \"] not found\");\n }\n };\n /**\n * Add a new component constructor to the registry and ensure that it extends the correct parent class\n * @protected\n * @param name\n * @param component\n */\n obj.set = function(name, component) {\n if (component) {\n if (typeof component != \"function\"){\n throw(\"unable to set dashboard component [\" + name + \"], argument provided is not a function\");\n } else {\n components[name] = component;\n components[name].prototype = new LocusZoom.Dashboard.Component();\n }\n } else {\n delete components[name];\n }\n };\n\n /**\n * Register a new component constructor by name\n * @param {String} name\n * @param {function} component The component constructor\n */\n obj.add = function(name, component) {\n if (components[name]) {\n throw(\"dashboard component already exists with name: \" + name);\n } else {\n obj.set(name, component);\n }\n };\n\n /**\n * List the names of all registered components\n * @returns {String[]}\n */\n obj.list = function() {\n return Object.keys(components);\n };\n\n return obj;\n})();\n\n/**\n * Plots and panels may have a \"dashboard\" element suited for showing HTML components that may be interactive.\n * When components need to incorporate a generic button, or additionally a button that generates a menu, this\n * class provides much of the necessary framework.\n * @class\n * @param {LocusZoom.Dashboard.Component} parent\n */\nLocusZoom.Dashboard.Component.Button = function(parent) { \n \n if (!(parent instanceof LocusZoom.Dashboard.Component)){\n throw \"Unable to create dashboard component button, invalid parent\";\n }\n /** @member {LocusZoom.Dashboard.Component} */\n this.parent = parent;\n /** @member {LocusZoom.Dashboard.Panel} */\n this.parent_panel = this.parent.parent_panel;\n /** @member {LocusZoom.Dashboard.Plot} */\n this.parent_plot = this.parent.parent_plot;\n /** @member {LocusZoom.Plot|LocusZoom.Panel} */\n this.parent_svg = this.parent.parent_svg;\n\n /** @member {LocusZoom.Dashboard|null|*} */\n this.parent_dashboard = this.parent.parent;\n /** @member {d3.selection} */\n this.selector = null;\n\n /**\n * Tag to use for the button (default: a)\n * @member {String}\n */\n this.tag = \"a\";\n\n /**\n * TODO This method does not appear to be used anywhere\n * @param {String} tag\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setTag = function(tag){\n if (typeof tag != \"undefined\"){ this.tag = tag.toString(); }\n return this;\n };\n\n /**\n * HTML for the button to show.\n * @protected\n * @member {String}\n */\n this.html = \"\";\n /**\n * Specify the HTML content of this button.\n * WARNING: The string provided will be inserted into the document as raw markup; XSS mitigation is the\n * responsibility of each button implementation.\n * @param {String} html\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setHtml = function(html){\n if (typeof html != \"undefined\"){ this.html = html.toString(); }\n return this;\n };\n /**\n * @deprecated since 0.5.6; use setHTML instead\n */\n this.setText = this.setHTML;\n\n /**\n * Mouseover title text for the button to show\n * @protected\n * @member {String}\n */\n this.title = \"\";\n /**\n * Set the mouseover title text for the button (if any)\n * @param {String} title Simple text to display\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setTitle = function(title){\n if (typeof title != \"undefined\"){ this.title = title.toString(); }\n return this;\n };\n\n /**\n * Color of the button\n * @member {String}\n */\n this.color = \"gray\";\n\n /**\n * Set the color associated with this button\n * @param {('gray'|'red'|'orange'|'yellow'|'green'|'blue'|'purple')} color Any selection not in the preset list\n * will be replaced with gray.\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setColor = function(color){\n if (typeof color != \"undefined\"){\n if ([\"gray\", \"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"purple\"].indexOf(color) !== -1){ this.color = color; }\n else { this.color = \"gray\"; }\n }\n return this;\n };\n\n /**\n * Hash of arbitrary button styles to apply as {name: value} entries\n * @protected\n * @member {Object}\n */\n this.style = {};\n /**\n * Set a collection of custom styles to be used by the button\n * @param {Object} style Hash of {name:value} entries\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setStyle = function(style){\n if (typeof style != \"undefined\"){ this.style = style; }\n return this;\n };\n\n //\n /**\n * Method to generate a CSS class string\n * @returns {string}\n */\n this.getClass = function(){\n var group_position = ([\"start\",\"middle\",\"end\"].indexOf(this.parent.layout.group_position) !== -1 ? \" lz-dashboard-button-group-\" + this.parent.layout.group_position : \"\");\n return \"lz-dashboard-button lz-dashboard-button-\" + this.color + (this.status ? \"-\" + this.status : \"\") + group_position;\n };\n\n // Permanence\n /**\n * Track internal state on whether to keep showing the button/ menu contents at the moment\n * @protected\n * @member {Boolean}\n */\n this.persist = false;\n /**\n * Configuration when defining a button: track whether this component should be allowed to keep open\n * menu/button contents in response to certain events\n * @protected\n * @member {Boolean}\n */\n this.permanent = false;\n /**\n * Allow code to change whether the button is allowed to be `permanent`\n * @param {boolean} bool\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.setPermanent = function(bool){\n if (typeof bool == \"undefined\"){ bool = true; } else { bool = Boolean(bool); }\n this.permanent = bool;\n if (this.permanent){ this.persist = true; }\n return this;\n };\n /**\n * Determine whether the button/menu contents should persist in response to a specific event\n * @returns {Boolean}\n */\n this.shouldPersist = function(){\n return this.permanent || this.persist;\n };\n\n /**\n * Button status (highlighted / disabled/ etc)\n * @protected\n * @member {String}\n */\n this.status = \"\";\n /**\n * Change button state\n * @param {('highlighted'|'disabled'|'')} status\n */\n this.setStatus = function(status){\n if (typeof status != \"undefined\" && [\"\", \"highlighted\", \"disabled\"].indexOf(status) !== -1){ this.status = status; }\n return this.update();\n };\n /**\n * Toggle whether the button is highlighted\n * @param {boolean} bool If provided, explicitly set highlighted state\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.highlight = function(bool){\n if (typeof bool == \"undefined\"){ bool = true; } else { bool = Boolean(bool); }\n if (bool){ return this.setStatus(\"highlighted\"); }\n else if (this.status === \"highlighted\"){ return this.setStatus(\"\"); }\n return this;\n };\n /**\n * Toggle whether the button is disabled\n * @param {boolean} bool If provided, explicitly set disabled state\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.disable = function(bool){\n if (typeof bool == \"undefined\"){ bool = true; } else { bool = Boolean(bool); }\n if (bool){ return this.setStatus(\"disabled\"); }\n else if (this.status === \"disabled\"){ return this.setStatus(\"\"); }\n return this;\n };\n\n // Mouse events\n /** @member {function} */\n this.onmouseover = function(){};\n this.setOnMouseover = function(onmouseover){\n if (typeof onmouseover == \"function\"){ this.onmouseover = onmouseover; }\n else { this.onmouseover = function(){}; }\n return this;\n };\n /** @member {function} */\n this.onmouseout = function(){};\n this.setOnMouseout = function(onmouseout){\n if (typeof onmouseout == \"function\"){ this.onmouseout = onmouseout; }\n else { this.onmouseout = function(){}; }\n return this;\n };\n /** @member {function} */\n this.onclick = function(){};\n this.setOnclick = function(onclick){\n if (typeof onclick == \"function\"){ this.onclick = onclick; }\n else { this.onclick = function(){}; }\n return this;\n };\n \n // Primary behavior functions\n /**\n * Show the button, including creating DOM elements if necessary for first render\n */\n this.show = function(){\n if (!this.parent){ return; }\n if (!this.selector){\n this.selector = this.parent.selector.append(this.tag).attr(\"class\", this.getClass());\n }\n return this.update();\n };\n /**\n * Hook for any actions or state cleanup to be performed before rerendering\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.preUpdate = function(){ return this; };\n /**\n * Update button state and contents, and fully rerender\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.update = function(){\n if (!this.selector){ return this; }\n this.preUpdate();\n this.selector\n .attr(\"class\", this.getClass())\n .attr(\"title\", this.title).style(this.style)\n .on(\"mouseover\", (this.status === \"disabled\") ? null : this.onmouseover)\n .on(\"mouseout\", (this.status === \"disabled\") ? null : this.onmouseout)\n .on(\"click\", (this.status === \"disabled\") ? null : this.onclick)\n .html(this.html);\n this.menu.update();\n this.postUpdate();\n return this;\n };\n /**\n * Hook for any behavior to be added/changed after the button has been re-rendered\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.postUpdate = function(){ return this; };\n /**\n * Hide the button by removing it from the DOM (may be overridden by current persistence setting)\n * @returns {LocusZoom.Dashboard.Component.Button}\n */\n this.hide = function(){\n if (this.selector && !this.shouldPersist()){\n this.selector.remove();\n this.selector = null;\n }\n return this;\n }; \n\n /**\n * Button Menu Object\n * The menu is an HTML overlay that can appear below a button. It can contain arbitrary HTML and\n * has logic to be automatically positioned and sized to behave more or less like a dropdown menu.\n * @member {Object}\n */\n this.menu = {\n outer_selector: null,\n inner_selector: null,\n scroll_position: 0,\n hidden: true,\n /**\n * Show the button menu, including setting up any DOM elements needed for first rendering\n */\n show: function(){\n if (!this.menu.outer_selector){\n this.menu.outer_selector = d3.select(this.parent_plot.svg.node().parentNode).append(\"div\")\n .attr(\"class\", \"lz-dashboard-menu lz-dashboard-menu-\" + this.color)\n .attr(\"id\", this.parent_svg.getBaseId() + \".dashboard.menu\");\n this.menu.inner_selector = this.menu.outer_selector.append(\"div\")\n .attr(\"class\", \"lz-dashboard-menu-content\");\n this.menu.inner_selector.on(\"scroll\", function(){\n this.menu.scroll_position = this.menu.inner_selector.node().scrollTop;\n }.bind(this));\n }\n this.menu.outer_selector.style({ visibility: \"visible\" });\n this.menu.hidden = false;\n return this.menu.update();\n }.bind(this),\n /**\n * Update the rendering of the menu\n */\n update: function(){\n if (!this.menu.outer_selector){ return this.menu; }\n this.menu.populate(); // This function is stubbed for all buttons by default and custom implemented in component definition\n if (this.menu.inner_selector){ this.menu.inner_selector.node().scrollTop = this.menu.scroll_position; }\n return this.menu.position();\n }.bind(this),\n position: function(){\n if (!this.menu.outer_selector){ return this.menu; }\n // Unset any explicitly defined outer selector height so that menus dynamically shrink if content is removed\n this.menu.outer_selector.style({ height: null });\n var padding = 3;\n var scrollbar_padding = 20;\n var menu_height_padding = 14; // 14: 2x 6px padding, 2x 1px border\n var page_origin = this.parent_svg.getPageOrigin();\n var page_scroll_top = document.documentElement.scrollTop || document.body.scrollTop;\n var container_offset = this.parent_plot.getContainerOffset();\n var dashboard_client_rect = this.parent_dashboard.selector.node().getBoundingClientRect();\n var button_client_rect = this.selector.node().getBoundingClientRect();\n var menu_client_rect = this.menu.outer_selector.node().getBoundingClientRect();\n var total_content_height = this.menu.inner_selector.node().scrollHeight;\n var top = 0; var left = 0;\n if (this.parent_dashboard.type === \"panel\"){\n top = (page_origin.y + dashboard_client_rect.height + (2 * padding));\n left = Math.max(page_origin.x + this.parent_svg.layout.width - menu_client_rect.width - padding, page_origin.x + padding);\n } else {\n top = button_client_rect.bottom + page_scroll_top + padding - container_offset.top;\n left = Math.max(button_client_rect.left + button_client_rect.width - menu_client_rect.width - container_offset.left, page_origin.x + padding);\n }\n var base_max_width = Math.max(this.parent_svg.layout.width - (2 * padding) - scrollbar_padding, scrollbar_padding);\n var container_max_width = base_max_width;\n var content_max_width = (base_max_width - (4 * padding));\n var base_max_height = Math.max(this.parent_svg.layout.height - (10 * padding) - menu_height_padding, menu_height_padding);\n var height = Math.min(total_content_height, base_max_height);\n var max_height = base_max_height;\n this.menu.outer_selector.style({\n \"top\": top.toString() + \"px\",\n \"left\": left.toString() + \"px\",\n \"max-width\": container_max_width.toString() + \"px\",\n \"max-height\": max_height.toString() + \"px\",\n \"height\": height.toString() + \"px\"\n });\n this.menu.inner_selector.style({ \"max-width\": content_max_width.toString() + \"px\" });\n this.menu.inner_selector.node().scrollTop = this.menu.scroll_position;\n return this.menu;\n }.bind(this),\n hide: function(){\n if (!this.menu.outer_selector){ return this.menu; }\n this.menu.outer_selector.style({ visibility: \"hidden\" });\n this.menu.hidden = true;\n return this.menu;\n }.bind(this),\n destroy: function(){\n if (!this.menu.outer_selector){ return this.menu; }\n this.menu.inner_selector.remove();\n this.menu.outer_selector.remove();\n this.menu.inner_selector = null;\n this.menu.outer_selector = null;\n return this.menu;\n }.bind(this),\n /**\n * Internal method definition\n * By convention populate() does nothing and should be reimplemented with each dashboard button definition\n * Reimplement by way of Dashboard.Component.Button.menu.setPopulate to define the populate method and hook\n * up standard menu click-toggle behavior prototype.\n * @protected\n */\n populate: function(){ /* stub */ }.bind(this),\n /**\n * Define how the menu is populated with items, and set up click and display properties as appropriate\n * @public\n */\n setPopulate: function(menu_populate_function){\n if (typeof menu_populate_function == \"function\"){\n this.menu.populate = menu_populate_function;\n this.setOnclick(function(){\n if (this.menu.hidden){\n this.menu.show();\n this.highlight().update();\n this.persist = true;\n } else {\n this.menu.hide();\n this.highlight(false).update();\n if (!this.permanent){ this.persist = false; }\n }\n }.bind(this));\n } else {\n this.setOnclick();\n }\n return this;\n }.bind(this)\n };\n\n};\n\n/**\n * Renders arbitrary text with title formatting\n * @class LocusZoom.Dashboard.Components.title\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {string} layout.title Text to render\n */\nLocusZoom.Dashboard.Components.add(\"title\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.show = function(){\n this.div_selector = this.parent.selector.append(\"div\")\n .attr(\"class\", \"lz-dashboard-title lz-dashboard-\" + this.layout.position);\n this.title_selector = this.div_selector.append(\"h3\");\n return this.update();\n };\n this.update = function(){\n var title = layout.title.toString();\n if (this.layout.subtitle){ title += \" \" + this.layout.subtitle + \"\"; }\n this.title_selector.html(title);\n return this;\n };\n});\n\n/**\n * Renders text to display the current dimensions of the plot. Automatically updated as plot dimensions change\n * @class LocusZoom.Dashboard.Components.dimensions\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"dimensions\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n var display_width = this.parent_plot.layout.width.toString().indexOf(\".\") === -1 ? this.parent_plot.layout.width : this.parent_plot.layout.width.toFixed(2);\n var display_height = this.parent_plot.layout.height.toString().indexOf(\".\") === -1 ? this.parent_plot.layout.height : this.parent_plot.layout.height.toFixed(2);\n this.selector.html(display_width + \"px × \" + display_height + \"px\");\n if (layout.class){ this.selector.attr(\"class\", layout.class); }\n if (layout.style){ this.selector.style(layout.style); }\n return this;\n };\n});\n\n/**\n * Display the current scale of the genome region displayed in the plot, as defined by the difference between\n * `state.end` and `state.start`.\n * @class LocusZoom.Dashboard.Components.region_scale\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"region_scale\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (!isNaN(this.parent_plot.state.start) && !isNaN(this.parent_plot.state.end)\n && this.parent_plot.state.start !== null && this.parent_plot.state.end !== null){\n this.selector.style(\"display\", null);\n this.selector.html(LocusZoom.positionIntToString(this.parent_plot.state.end - this.parent_plot.state.start, null, true));\n } else {\n this.selector.style(\"display\", \"none\");\n }\n if (layout.class){ this.selector.attr(\"class\", layout.class); }\n if (layout.style){ this.selector.style(layout.style); }\n return this;\n };\n});\n\n/**\n * Button to export current plot to an SVG image\n * @class LocusZoom.Dashboard.Components.download\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"download\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (this.button){ return this; }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(\"Download Image\").setTitle(\"Download image of the current plot as locuszoom.svg\")\n .setOnMouseover(function() {\n this.button.selector\n .classed(\"lz-dashboard-button-gray-disabled\", true)\n .html(\"Preparing Image\");\n this.generateBase64SVG().then(function(base64_string){\n this.button.selector\n .attr(\"href\", \"data:image/svg+xml;base64,\\n\" + base64_string)\n .classed(\"lz-dashboard-button-gray-disabled\", false)\n .classed(\"lz-dashboard-button-gray-highlighted\", true)\n .html(\"Download Image\");\n }.bind(this));\n }.bind(this))\n .setOnMouseout(function() {\n this.button.selector.classed(\"lz-dashboard-button-gray-highlighted\", false);\n }.bind(this));\n this.button.show();\n this.button.selector.attr(\"href-lang\", \"image/svg+xml\").attr(\"download\", \"locuszoom.svg\");\n return this;\n };\n this.css_string = \"\";\n for (var stylesheet in Object.keys(document.styleSheets)){\n if ( document.styleSheets[stylesheet].href !== null\n && document.styleSheets[stylesheet].href.indexOf(\"locuszoom.css\") !== -1){\n LocusZoom.createCORSPromise(\"GET\", document.styleSheets[stylesheet].href)\n .then(function(response){\n this.css_string = response.replace(/[\\r\\n]/g,\" \").replace(/\\s+/g,\" \");\n if (this.css_string.indexOf(\"/* ! LocusZoom HTML Styles */\")){\n this.css_string = this.css_string.substring(0, this.css_string.indexOf(\"/* ! LocusZoom HTML Styles */\"));\n }\n }.bind(this));\n break;\n }\n } \n this.generateBase64SVG = function(){\n return Q.fcall(function () {\n // Insert a hidden div, clone the node into that so we can modify it with d3\n var container = this.parent.selector.append(\"div\").style(\"display\", \"none\")\n .html(this.parent_plot.svg.node().outerHTML);\n // Remove unnecessary elements\n container.selectAll(\"g.lz-curtain\").remove();\n container.selectAll(\"g.lz-mouse_guide\").remove();\n // Convert units on axis tick dy attributes from ems to pixels\n container.selectAll(\"g.tick text\").each(function(){\n var dy = +(d3.select(this).attr(\"dy\").substring(-2).slice(0,-2))*10;\n d3.select(this).attr(\"dy\", dy);\n });\n // Pull the svg into a string and add the contents of the locuszoom stylesheet\n // Don't add this with d3 because it will escape the CDATA declaration incorrectly\n var initial_html = d3.select(container.select(\"svg\").node().parentNode).html();\n var style_def = \"\";\n var insert_at = initial_html.indexOf(\">\") + 1;\n initial_html = initial_html.slice(0,insert_at) + style_def + initial_html.slice(insert_at);\n // Delete the container node\n container.remove();\n // Base64-encode the string and return it\n return btoa(encodeURIComponent(initial_html).replace(/%([0-9A-F]{2})/g, function(match, p1) {\n return String.fromCharCode(\"0x\" + p1);\n }));\n }.bind(this));\n };\n});\n\n/**\n * Button to remove panel from plot.\n * NOTE: Will only work on panel dashboards.\n * @class LocusZoom.Dashboard.Components.remove_panel\n * @augments LocusZoom.Dashboard.Component\n * @param {Boolean} [layout.suppress_confirm=false] If true, removes the panel without prompting user for confirmation\n */\nLocusZoom.Dashboard.Components.add(\"remove_panel\", function(layout) {\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function() {\n if (this.button){ return this; }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(\"×\").setTitle(\"Remove panel\")\n .setOnclick(function(){\n if (!layout.suppress_confirm && !confirm(\"Are you sure you want to remove this panel? This cannot be undone!\")){\n return false;\n }\n var panel = this.parent_panel;\n panel.dashboard.hide(true);\n d3.select(panel.parent.svg.node().parentNode).on(\"mouseover.\" + panel.getBaseId() + \".dashboard\", null);\n d3.select(panel.parent.svg.node().parentNode).on(\"mouseout.\" + panel.getBaseId() + \".dashboard\", null);\n return panel.parent.removePanel(panel.id);\n }.bind(this));\n this.button.show();\n return this;\n };\n});\n\n/**\n * Button to move panel up relative to other panels (in terms of y-index on the page)\n * NOTE: Will only work on panel dashboards.\n * @class LocusZoom.Dashboard.Components.move_panel_up\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"move_panel_up\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (this.button){\n var is_at_top = (this.parent_panel.layout.y_index === 0);\n this.button.disable(is_at_top);\n return this;\n }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(\"▴\").setTitle(\"Move panel up\")\n .setOnclick(function(){\n this.parent_panel.moveUp();\n this.update();\n }.bind(this));\n this.button.show();\n return this.update();\n };\n});\n\n/**\n * Button to move panel down relative to other panels (in terms of y-index on the page)\n * NOTE: Will only work on panel dashboards.\n * @class LocusZoom.Dashboard.Components.move_panel_down\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"move_panel_down\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (this.button){\n var is_at_bottom = (this.parent_panel.layout.y_index === this.parent_plot.panel_ids_by_y_index.length-1);\n this.button.disable(is_at_bottom);\n return this;\n }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(\"▾\").setTitle(\"Move panel down\")\n .setOnclick(function(){\n this.parent_panel.moveDown();\n this.update();\n }.bind(this));\n this.button.show();\n return this.update();\n };\n});\n\n/**\n * Button to shift plot region forwards or back by a `step` increment provided in the layout\n * @class LocusZoom.Dashboard.Components.shift_region\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {number} [layout.step=50000] The stepsize to change the region by\n * @param {string} [layout.button_html]\n * @param {string} [layout.button_title]\n */\nLocusZoom.Dashboard.Components.add(\"shift_region\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)){\n this.update = function(){};\n console.warn(\"Unable to add shift_region dashboard component: plot state does not have region bounds\");\n return;\n }\n if (isNaN(layout.step) || layout.step === 0){ layout.step = 50000; }\n if (typeof layout.button_html !== \"string\"){ layout.button_html = layout.step > 0 ? \">\" : \"<\"; }\n if (typeof layout.button_title !== \"string\"){\n layout.button_title = \"Shift region by \" + (layout.step > 0 ? \"+\" : \"-\") + LocusZoom.positionIntToString(Math.abs(layout.step),null,true);\n }\n this.update = function(){\n if (this.button){ return this; }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title)\n .setOnclick(function(){\n this.parent_plot.applyState({\n start: Math.max(this.parent_plot.state.start + layout.step, 1),\n end: this.parent_plot.state.end + layout.step\n });\n }.bind(this));\n this.button.show();\n return this;\n };\n});\n\n/**\n * Zoom in or out on the plot, centered on the middle of the plot region, by the specified amount\n * @class LocusZoom.Dashboard.Components.zoom_region\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {number} [layout.step=0.2] The amount to zoom in by (where 1 indicates 100%)\n */\nLocusZoom.Dashboard.Components.add(\"zoom_region\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n if (isNaN(this.parent_plot.state.start) || isNaN(this.parent_plot.state.end)){\n this.update = function(){};\n console.warn(\"Unable to add zoom_region dashboard component: plot state does not have region bounds\");\n return;\n }\n if (isNaN(layout.step) || layout.step === 0){ layout.step = 0.2; }\n if (typeof layout.button_html != \"string\"){ layout.button_html = layout.step > 0 ? \"z–\" : \"z+\"; }\n if (typeof layout.button_title != \"string\"){\n layout.button_title = \"Zoom region \" + (layout.step > 0 ? \"out\" : \"in\") + \" by \" + (Math.abs(layout.step)*100).toFixed(1) + \"%\";\n }\n this.update = function(){\n if (this.button){\n var can_zoom = true;\n var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start;\n if (layout.step > 0 && !isNaN(this.parent_plot.layout.max_region_scale) && current_region_scale >= this.parent_plot.layout.max_region_scale){\n can_zoom = false;\n }\n if (layout.step < 0 && !isNaN(this.parent_plot.layout.min_region_scale) && current_region_scale <= this.parent_plot.layout.min_region_scale){\n can_zoom = false;\n }\n this.button.disable(!can_zoom);\n return this;\n }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title)\n .setOnclick(function(){\n var current_region_scale = this.parent_plot.state.end - this.parent_plot.state.start;\n var zoom_factor = 1 + layout.step;\n var new_region_scale = current_region_scale * zoom_factor;\n if (!isNaN(this.parent_plot.layout.max_region_scale)){\n new_region_scale = Math.min(new_region_scale, this.parent_plot.layout.max_region_scale);\n }\n if (!isNaN(this.parent_plot.layout.min_region_scale)){\n new_region_scale = Math.max(new_region_scale, this.parent_plot.layout.min_region_scale);\n }\n var delta = Math.floor((new_region_scale - current_region_scale) / 2);\n this.parent_plot.applyState({\n start: Math.max(this.parent_plot.state.start - delta, 1),\n end: this.parent_plot.state.end + delta\n });\n }.bind(this));\n this.button.show();\n return this;\n };\n});\n\n/**\n * Renders button with arbitrary text that, when clicked, shows a dropdown containing arbitrary HTML\n * NOTE: Trusts content exactly as given. XSS prevention is the responsibility of the implementer.\n * @class LocusZoom.Dashboard.Components.menu\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {string} layout.button_html The HTML to render inside the button\n * @param {string} layout.button_title Text to display as a tooltip when hovering over the button\n * @param {string} layout.menu_html The HTML content of the dropdown menu\n */\nLocusZoom.Dashboard.Components.add(\"menu\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (this.button){ return this; }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title);\n this.button.menu.setPopulate(function(){\n this.button.menu.inner_selector.html(layout.menu_html);\n }.bind(this));\n this.button.show();\n return this;\n };\n});\n\n/**\n * Special button/menu to allow model building by tracking individual covariants. Will track a list of covariate\n * objects and store them in the special `model.covariates` field of plot `state`.\n * @class LocusZoom.Dashboard.Components.covariates_model\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {string} layout.button_html The HTML to render inside the button\n * @param {string} layout.button_title Text to display as a tooltip when hovering over the button\n */\nLocusZoom.Dashboard.Components.add(\"covariates_model\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n\n this.initialize = function(){\n // Initialize state.model.covariates\n this.parent_plot.state.model = this.parent_plot.state.model || {};\n this.parent_plot.state.model.covariates = this.parent_plot.state.model.covariates || [];\n // Create an object at the plot level for easy access to interface methods in custom client-side JS\n /**\n * When a covariates model dashboard element is present, create (one) object at the plot level that exposes\n * component data and state for custom interactions with other plot elements.\n * @class LocusZoom.Plot.CovariatesModel\n */\n this.parent_plot.CovariatesModel = {\n /** @member {LocusZoom.Dashboard.Component.Button} */\n button: this,\n /**\n * Add an element to the model and show a representation of it in the dashboard component menu. If the\n * element is already part of the model, do nothing (to avoid adding duplicates).\n * When plot state is changed, this will automatically trigger requests for new data accordingly.\n * @param {string|object} element_reference Can be any value that can be put through JSON.stringify()\n * to create a serialized representation of itself.\n */\n add: function(element_reference){\n var element = JSON.parse(JSON.stringify(element_reference));\n if (typeof element_reference == \"object\" && typeof element.html != \"string\"){\n element.html = ( (typeof element_reference.toHTML == \"function\") ? element_reference.toHTML() : element_reference.toString());\n }\n // Check if the element is already in the model covariates array and return if it is.\n for (var i = 0; i < this.state.model.covariates.length; i++) {\n if (JSON.stringify(this.state.model.covariates[i]) === JSON.stringify(element)) {\n return this;\n }\n }\n this.state.model.covariates.push(element);\n this.applyState();\n this.CovariatesModel.updateComponent();\n return this;\n }.bind(this.parent_plot),\n /**\n * Remove an element from `state.model.covariates` (and from the dashboard component menu's\n * representation of the state model). When plot state is changed, this will automatically trigger\n * requests for new data accordingly.\n * @param {number} idx Array index of the element, in the `state.model.covariates array`.\n */\n removeByIdx: function(idx){\n if (typeof this.state.model.covariates[idx] == \"undefined\"){\n throw(\"Unable to remove model covariate, invalid index: \" + idx.toString());\n }\n this.state.model.covariates.splice(idx, 1);\n this.applyState();\n this.CovariatesModel.updateComponent();\n return this;\n }.bind(this.parent_plot),\n /**\n * Empty the `state.model.covariates` array (and dashboard component menu representation thereof) of all\n * elements. When plot state is changed, this will automatically trigger requests for new data accordingly\n */\n removeAll: function(){\n this.state.model.covariates = [];\n this.applyState();\n this.CovariatesModel.updateComponent();\n return this;\n }.bind(this.parent_plot),\n /**\n * Manually trigger the update methods on the dashboard component's button and menu elements to force\n * display of most up-to-date content. Can be used to force the dashboard to reflect changes made, eg if\n * modifying `state.model.covariates` directly instead of via `plot.CovariatesModel`\n */\n updateComponent: function(){\n this.button.update();\n this.button.menu.update();\n }.bind(this)\n };\n }.bind(this);\n\n this.update = function(){\n\n if (this.button){ return this; }\n\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title)\n .setOnclick(function(){\n this.button.menu.populate();\n }.bind(this));\n\n this.button.menu.setPopulate(function(){\n var selector = this.button.menu.inner_selector;\n selector.html(\"\");\n // General model HTML representation\n if (typeof this.parent_plot.state.model.html != \"undefined\"){\n selector.append(\"div\").html(this.parent_plot.state.model.html);\n }\n // Model covariates table\n if (!this.parent_plot.state.model.covariates.length){\n selector.append(\"i\").html(\"no covariates in model\");\n } else {\n selector.append(\"h5\").html(\"Model Covariates (\" + this.parent_plot.state.model.covariates.length + \")\");\n var table = selector.append(\"table\");\n this.parent_plot.state.model.covariates.forEach(function(covariate, idx){\n var html = ( (typeof covariate == \"object\" && typeof covariate.html == \"string\") ? covariate.html : covariate.toString() );\n var row = table.append(\"tr\");\n row.append(\"td\").append(\"button\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-\" + this.layout.color)\n .style({ \"margin-left\": \"0em\" })\n .on(\"click\", function(){\n this.parent_plot.CovariatesModel.removeByIdx(idx);\n }.bind(this))\n .html(\"×\");\n row.append(\"td\").html(html);\n }.bind(this));\n selector.append(\"button\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-\" + this.layout.color)\n .style({ \"margin-left\": \"4px\" }).html(\"× Remove All Covariates\")\n .on(\"click\", function(){\n this.parent_plot.CovariatesModel.removeAll();\n }.bind(this));\n }\n }.bind(this));\n\n this.button.preUpdate = function(){\n var html = \"Model\";\n if (this.parent_plot.state.model.covariates.length){\n var cov = this.parent_plot.state.model.covariates.length > 1 ? \"covariates\" : \"covariate\";\n html += \" (\" + this.parent_plot.state.model.covariates.length + \" \" + cov + \")\";\n }\n this.button.setHtml(html).disable(false);\n }.bind(this);\n\n this.button.show();\n\n return this;\n };\n});\n\n/**\n * Button to toggle split tracks\n * @class LocusZoom.Dashboard.Components.toggle_split_tracks\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"toggle_split_tracks\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n if (!layout.data_layer_id){ layout.data_layer_id = \"intervals\"; }\n if (!this.parent_panel.data_layers[layout.data_layer_id]){\n throw (\"Dashboard toggle split tracks component missing valid data layer ID\");\n }\n this.update = function(){\n var data_layer = this.parent_panel.data_layers[layout.data_layer_id];\n var html = data_layer.layout.split_tracks ? \"Merge Tracks\" : \"Split Tracks\";\n if (this.button){\n this.button.setHtml(html);\n this.button.show();\n this.parent.position();\n return this;\n } else {\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(html)\n .setTitle(\"Toggle whether tracks are split apart or merged together\")\n .setOnclick(function(){\n data_layer.toggleSplitTracks();\n if (this.scale_timeout){ clearTimeout(this.scale_timeout); }\n var timeout = data_layer.layout.transition ? +data_layer.layout.transition.duration || 0 : 0;\n this.scale_timeout = setTimeout(function(){\n this.parent_panel.scaleHeightToData();\n this.parent_plot.positionPanels();\n }.bind(this), timeout);\n this.update();\n }.bind(this));\n return this.update();\n }\n };\n});\n\n/**\n * Button to resize panel height to fit available data (eg when showing a list of tracks)\n * @class LocusZoom.Dashboard.Components.resize_to_data\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"resize_to_data\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n if (this.button){ return this; }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(\"Resize to Data\")\n .setTitle(\"Automatically resize this panel to fit the data its currently showing\")\n .setOnclick(function(){\n this.parent_panel.scaleHeightToData();\n this.update();\n }.bind(this));\n this.button.show();\n return this;\n };\n});\n\n/**\n * Button to toggle legend\n * @class LocusZoom.Dashboard.Components.toggle_legend\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"toggle_legend\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n this.update = function(){\n var html = this.parent_panel.legend.layout.hidden ? \"Show Legend\" : \"Hide Legend\";\n if (this.button){\n this.button.setHtml(html).show();\n this.parent.position();\n return this;\n }\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color)\n .setTitle(\"Show or hide the legend for this panel\")\n .setOnclick(function(){\n this.parent_panel.legend.layout.hidden = !this.parent_panel.legend.layout.hidden;\n this.parent_panel.legend.render();\n this.update();\n }.bind(this));\n return this.update();\n };\n});\n\n/**\n * Menu for manipulating multiple data layers in a single panel: show/hide, change order, etc.\n * @class LocusZoom.Dashboard.Components.data_layers\n * @augments LocusZoom.Dashboard.Component\n */\nLocusZoom.Dashboard.Components.add(\"data_layers\", function(layout){\n LocusZoom.Dashboard.Component.apply(this, arguments);\n\n this.update = function(){\n\n if (typeof layout.button_html != \"string\"){ layout.button_html = \"Data Layers\"; }\n if (typeof layout.button_title != \"string\"){ layout.button_title = \"Manipulate Data Layers (sort, dim, show/hide, etc.)\"; }\n\n if (this.button){ return this; }\n\n this.button = new LocusZoom.Dashboard.Component.Button(this)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title)\n .setOnclick(function(){\n this.button.menu.populate();\n }.bind(this));\n\n this.button.menu.setPopulate(function(){\n this.button.menu.inner_selector.html(\"\");\n var table = this.button.menu.inner_selector.append(\"table\");\n this.parent_panel.data_layer_ids_by_z_index.slice().reverse().forEach(function(id, idx){\n var data_layer = this.parent_panel.data_layers[id];\n var name = (typeof data_layer.layout.name != \"string\") ? data_layer.id : data_layer.layout.name;\n var row = table.append(\"tr\");\n // Layer name\n row.append(\"td\").html(name);\n // Status toggle buttons\n layout.statuses.forEach(function(status_adj){\n var status_idx = LocusZoom.DataLayer.Statuses.adjectives.indexOf(status_adj);\n var status_verb = LocusZoom.DataLayer.Statuses.verbs[status_idx];\n var html, onclick, highlight;\n if (data_layer.global_statuses[status_adj]){\n html = LocusZoom.DataLayer.Statuses.menu_antiverbs[status_idx];\n onclick = \"un\" + status_verb + \"AllElements\";\n highlight = \"-highlighted\";\n } else {\n html = LocusZoom.DataLayer.Statuses.verbs[status_idx];\n onclick = status_verb + \"AllElements\";\n highlight = \"\";\n }\n row.append(\"td\").append(\"a\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-\" + this.layout.color + highlight)\n .style({ \"margin-left\": \"0em\" })\n .on(\"click\", function(){ data_layer[onclick](); this.button.menu.populate(); }.bind(this))\n .html(html);\n }.bind(this));\n // Sort layer buttons\n var at_top = (idx === 0);\n var at_bottom = (idx === (this.parent_panel.data_layer_ids_by_z_index.length - 1));\n var td = row.append(\"td\");\n td.append(\"a\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-group-start lz-dashboard-button-\" + this.layout.color + (at_bottom ? \"-disabled\" : \"\"))\n .style({ \"margin-left\": \"0em\" })\n .on(\"click\", function(){ data_layer.moveDown(); this.button.menu.populate(); }.bind(this))\n .html(\"▾\").attr(\"title\", \"Move layer down (further back)\");\n td.append(\"a\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-group-middle lz-dashboard-button-\" + this.layout.color + (at_top ? \"-disabled\" : \"\"))\n .style({ \"margin-left\": \"0em\" })\n .on(\"click\", function(){ data_layer.moveUp(); this.button.menu.populate(); }.bind(this))\n .html(\"▴\").attr(\"title\", \"Move layer up (further front)\");\n td.append(\"a\")\n .attr(\"class\", \"lz-dashboard-button lz-dashboard-button-group-end lz-dashboard-button-red\")\n .style({ \"margin-left\": \"0em\" })\n .on(\"click\", function(){\n if (confirm(\"Are you sure you want to remove the \" + name + \" layer? This cannot be undone!\")){\n data_layer.parent.removeDataLayer(id);\n }\n return this.button.menu.populate();\n }.bind(this))\n .html(\"×\").attr(\"title\", \"Remove layer\");\n }.bind(this));\n return this;\n }.bind(this));\n\n this.button.show();\n\n return this;\n };\n});\n\n/**\n * Dropdown menu allowing the user to choose between different display options for a single specific data layer\n * within a panel.\n *\n * This allows controlling how points on a datalayer can be displayed- any display options supported via the layout for the target datalayer. This includes point\n * size/shape, coloring, etc.\n *\n * This button intentionally limits display options it can control to those available on common plot types.\n * Although the list of options it sets can be overridden (to control very special custom plot types), this\n * capability should be used sparingly if at all.\n *\n * @class LocusZoom.Dashboard.Components.display_options\n * @augments LocusZoom.Dashboard.Component\n * @param {object} layout\n * @param {String} [layout.button_html=\"Display options\"] Text to display on the toolbar button\n * @param {String} [layout.button_title=\"Control how plot items are displayed\"] Hover text for the toolbar button\n * @param {string} layout.layer_name Specify the datalayer that this button should affect\n * @param {string} [layout.default_config_display_name] Store the default configuration for this datalayer\n * configuration, and show a button to revert to the \"default\" (listing the human-readable display name provided)\n * @param {Array} [layout.fields_whitelist='see code'] The list of presentation fields that this button can control.\n * This can be overridden if this button needs to be used on a custom layer type with special options.\n * @typedef {{display_name: string, display: Object}} DisplayOptionsButtonConfigField\n * @param {DisplayOptionsButtonConfigField[]} layout.options Specify a label and set of layout directives associated\n * with this `display` option. Display field should include all changes to datalayer presentation options.\n */\nLocusZoom.Dashboard.Components.add(\"display_options\", function (layout) {\n if (typeof layout.button_html != \"string\"){ layout.button_html = \"Display options\"; }\n if (typeof layout.button_title != \"string\"){ layout.button_title = \"Control how plot items are displayed\"; }\n\n // Call parent constructor\n LocusZoom.Dashboard.Component.apply(this, arguments);\n\n // List of layout fields that this button is allowed to control. This ensures that we don't override any other\n // information (like plot height etc) while changing point rendering\n var allowed_fields = layout.fields_whitelist || [\"color\", \"fill_opacity\", \"label\", \"legend\",\n \"point_shape\", \"point_size\", \"tooltip\", \"tooltip_positioning\"];\n\n var dataLayer = this.parent_panel.data_layers[layout.layer_name];\n var dataLayerLayout = dataLayer.layout;\n\n // Store default configuration for the layer as a clean deep copy, so we may revert later\n var defaultConfig = {};\n allowed_fields.forEach(function(name) {\n var configSlot = dataLayerLayout[name];\n if (configSlot) {\n defaultConfig[name] = JSON.parse(JSON.stringify(configSlot));\n }\n });\n\n /**\n * Which item in the menu is currently selected. (track for rerendering menu)\n * @member {String}\n * @private\n */\n this._selected_item = \"default\";\n\n // Define the button + menu that provides the real functionality for this dashboard component\n var self = this;\n this.button = new LocusZoom.Dashboard.Component.Button(self)\n .setColor(layout.color).setHtml(layout.button_html).setTitle(layout.button_title)\n .setOnclick(function () {\n self.button.menu.populate();\n });\n this.button.menu.setPopulate(function () {\n // Multiple copies of this button might be used on a single LZ page; append unique IDs where needed\n var uniqueID = Math.floor(Math.random() * 1e4).toString();\n\n self.button.menu.inner_selector.html(\"\");\n var table = self.button.menu.inner_selector.append(\"table\");\n\n var menuLayout = self.layout;\n\n var renderRow = function(display_name, display_options, row_id) { // Helper method\n var row = table.append(\"tr\");\n row.append(\"td\")\n .append(\"input\")\n .attr({type: \"radio\", name: \"color-picker-\" + uniqueID, value: row_id})\n .property(\"checked\", (row_id === self._selected_item))\n .on(\"click\", function () {\n Object.keys(display_options).forEach(function(field_name) {\n dataLayer.layout[field_name] = display_options[field_name];\n });\n self._selected_item = row_id;\n self.parent_panel.render();\n var legend = self.parent_panel.legend;\n if (legend && display_options.legend) {\n // Update the legend only if necessary\n legend.render();\n }\n });\n row.append(\"td\").text(display_name);\n };\n // Render the \"display options\" menu: default and special custom options\n var defaultName = menuLayout.default_config_display_name || \"Default style\";\n renderRow(defaultName, defaultConfig, \"default\");\n menuLayout.options.forEach(function (item, index) {\n renderRow(item.display_name, item.display, index);\n });\n return self;\n });\n\n this.update = function () {\n this.button.show();\n return this;\n };\n});\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * An SVG object used to display contextual information about a panel.\n * Panel layouts determine basic features of a legend - its position in the panel, orientation, title, etc.\n * Layouts of child data layers of the panel determine the actual content of the legend.\n *\n * @class\n * @param {LocusZoom.Panel} parent\n*/\nLocusZoom.Legend = function(parent){\n if (!(parent instanceof LocusZoom.Panel)){\n throw \"Unable to create legend, parent must be a locuszoom panel\";\n }\n /** @member {LocusZoom.Panel} */\n this.parent = parent;\n /** @member {String} */\n this.id = this.parent.getBaseId() + \".legend\";\n\n this.parent.layout.legend = LocusZoom.Layouts.merge(this.parent.layout.legend || {}, LocusZoom.Legend.DefaultLayout);\n /** @member {Object} */\n this.layout = this.parent.layout.legend;\n\n /** @member {d3.selection} */\n this.selector = null;\n /** @member {d3.selection} */\n this.background_rect = null;\n /** @member {d3.selection[]} */\n this.elements = [];\n /**\n * SVG selector for the group containing all elements in the legend\n * @protected\n * @member {d3.selection|null}\n */\n this.elements_group = null;\n\n /**\n * TODO: Not sure if this property is used; the external-facing methods are setting `layout.hidden` instead. Tentatively mark deprecated.\n * @deprecated\n * @protected\n * @member {Boolean}\n */\n this.hidden = false;\n\n // TODO Revisit constructor return value; see https://stackoverflow.com/a/3350364/1422268\n return this.render();\n};\n\n/**\n * The default layout used by legends (used internally)\n * @protected\n * @member {Object}\n */\nLocusZoom.Legend.DefaultLayout = {\n orientation: \"vertical\",\n origin: { x: 0, y: 0 },\n width: 10,\n height: 10,\n padding: 5,\n label_size: 12,\n hidden: false\n};\n\n/**\n * Render the legend in the parent panel\n */\nLocusZoom.Legend.prototype.render = function(){\n\n // Get a legend group selector if not yet defined\n if (!this.selector){\n this.selector = this.parent.svg.group.append(\"g\")\n .attr(\"id\", this.parent.getBaseId() + \".legend\").attr(\"class\", \"lz-legend\");\n }\n\n // Get a legend background rect selector if not yet defined\n if (!this.background_rect){\n this.background_rect = this.selector.append(\"rect\")\n .attr(\"width\", 100).attr(\"height\", 100).attr(\"class\", \"lz-legend-background\");\n }\n\n // Get a legend elements group selector if not yet defined\n if (!this.elements_group){\n this.elements_group = this.selector.append(\"g\");\n }\n\n // Remove all elements from the document and re-render from scratch\n this.elements.forEach(function(element){\n element.remove();\n });\n this.elements = [];\n\n // Gather all elements from data layers in order (top to bottom) and render them\n var padding = +this.layout.padding || 1;\n var x = padding;\n var y = padding;\n var line_height = 0;\n this.parent.data_layer_ids_by_z_index.slice().reverse().forEach(function(id){\n if (Array.isArray(this.parent.data_layers[id].layout.legend)){\n this.parent.data_layers[id].layout.legend.forEach(function(element){\n var selector = this.elements_group.append(\"g\")\n .attr(\"transform\", \"translate(\" + x + \",\" + y + \")\");\n var label_size = +element.label_size || +this.layout.label_size || 12;\n var label_x = 0;\n var label_y = (label_size/2) + (padding/2);\n line_height = Math.max(line_height, label_size + padding);\n // Draw the legend element symbol (line, rect, shape, etc)\n if (element.shape === \"line\"){\n // Line symbol\n var length = +element.length || 16;\n var path_y = (label_size/4) + (padding/2);\n selector.append(\"path\").attr(\"class\", element.class || \"\")\n .attr(\"d\", \"M0,\" + path_y + \"L\" + length + \",\" + path_y)\n .style(element.style || {});\n label_x = length + padding;\n } else if (element.shape === \"rect\"){\n // Rect symbol\n var width = +element.width || 16;\n var height = +element.height || width;\n selector.append(\"rect\").attr(\"class\", element.class || \"\")\n .attr(\"width\", width).attr(\"height\", height)\n .attr(\"fill\", element.color || {})\n .style(element.style || {});\n label_x = width + padding;\n line_height = Math.max(line_height, height + padding);\n } else if (d3.svg.symbolTypes.indexOf(element.shape) !== -1) {\n // Shape symbol (circle, diamond, etc.)\n var size = +element.size || 40;\n var radius = Math.ceil(Math.sqrt(size/Math.PI));\n selector.append(\"path\").attr(\"class\", element.class || \"\")\n .attr(\"d\", d3.svg.symbol().size(size).type(element.shape))\n .attr(\"transform\", \"translate(\" + radius + \",\" + (radius+(padding/2)) + \")\")\n .attr(\"fill\", element.color || {})\n .style(element.style || {});\n label_x = (2*radius) + padding;\n label_y = Math.max((2*radius)+(padding/2), label_y);\n line_height = Math.max(line_height, (2*radius) + padding);\n }\n // Draw the legend element label\n selector.append(\"text\").attr(\"text-anchor\", \"left\").attr(\"class\", \"lz-label\")\n .attr(\"x\", label_x).attr(\"y\", label_y).style({\"font-size\": label_size}).text(element.label);\n // Position the legend element group based on legend layout orientation\n var bcr = selector.node().getBoundingClientRect();\n if (this.layout.orientation === \"vertical\"){\n y += bcr.height + padding;\n line_height = 0;\n } else {\n // Ensure this element does not exceed the panel width\n // (E.g. drop to the next line if it does, but only if it's not the only element on this line)\n var right_x = this.layout.origin.x + x + bcr.width;\n if (x > padding && right_x > this.parent.layout.width){\n y += line_height;\n x = padding;\n selector.attr(\"transform\", \"translate(\" + x + \",\" + y + \")\");\n }\n x += bcr.width + (3*padding);\n }\n // Store the element\n this.elements.push(selector);\n }.bind(this));\n }\n }.bind(this));\n\n // Scale the background rect to the elements in the legend\n var bcr = this.elements_group.node().getBoundingClientRect();\n this.layout.width = bcr.width + (2*this.layout.padding);\n this.layout.height = bcr.height + (2*this.layout.padding);\n this.background_rect\n .attr(\"width\", this.layout.width)\n .attr(\"height\", this.layout.height);\n\n // Set the visibility on the legend from the \"hidden\" flag\n // TODO: `show()` and `hide()` call a full rerender; might be able to make this more lightweight?\n this.selector.style({ visibility: this.layout.hidden ? \"hidden\" : \"visible\" });\n\n // TODO: Annotate return type and make consistent\n return this.position();\n};\n\n/**\n * Place the legend in position relative to the panel, as specified in the layout configuration\n * @returns {LocusZoom.Legend | null}\n * TODO: should this always be chainable?\n */\nLocusZoom.Legend.prototype.position = function(){\n if (!this.selector){ return this; }\n var bcr = this.selector.node().getBoundingClientRect();\n if (!isNaN(+this.layout.pad_from_bottom)){\n this.layout.origin.y = this.parent.layout.height - bcr.height - +this.layout.pad_from_bottom;\n }\n if (!isNaN(+this.layout.pad_from_right)){\n this.layout.origin.x = this.parent.layout.width - bcr.width - +this.layout.pad_from_right;\n }\n this.selector.attr(\"transform\", \"translate(\" + this.layout.origin.x + \",\" + this.layout.origin.y + \")\");\n};\n\n/**\n * Hide the legend (triggers a re-render)\n * @public\n */\nLocusZoom.Legend.prototype.hide = function(){\n this.layout.hidden = true;\n this.render();\n};\n\n/**\n * Show the legend (triggers a re-render)\n * @public\n */\nLocusZoom.Legend.prototype.show = function(){\n this.layout.hidden = false;\n this.render();\n};\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * LocusZoom functionality used for data parsing and retrieval\n * @namespace\n * @public\n */\nLocusZoom.Data = LocusZoom.Data || {};\n\n/**\n * Create and coordinate an ensemble of (namespaced) data source instances\n * @public\n * @class\n */\nLocusZoom.DataSources = function() {\n /** @member {Object.} */\n this.sources = {};\n};\n\n/** @deprecated */\nLocusZoom.DataSources.prototype.addSource = function(ns, x) {\n console.warn(\"Warning: .addSource() is deprecated. Use .add() instead\");\n return this.add(ns, x);\n};\n\n/**\n * Add a (namespaced) datasource to the plot\n * @public\n * @param {String} ns A namespace used for fields from this data source\n * @param {LocusZoom.Data.Source|Array|null} x An instantiated datasource, or an array of arguments that can be used to\n * create a known datasource type.\n */\nLocusZoom.DataSources.prototype.add = function(ns, x) {\n return this.set(ns, x);\n};\n\n/** @protected */\nLocusZoom.DataSources.prototype.set = function(ns, x) {\n if (Array.isArray(x)) {\n var dsobj = LocusZoom.KnownDataSources.create.apply(null, x);\n this.sources[ns] = dsobj;\n } else {\n if (x !== null) {\n this.sources[ns] = x;\n } else {\n delete this.sources[ns];\n }\n }\n return this;\n};\n\n/** @deprecated */\nLocusZoom.DataSources.prototype.getSource = function(ns) {\n console.warn(\"Warning: .getSource() is deprecated. Use .get() instead\");\n return this.get(ns);\n};\n\n/**\n * Return the datasource associated with a given namespace\n * @public\n * @param {String} ns Namespace\n * @returns {LocusZoom.Data.Source}\n */\nLocusZoom.DataSources.prototype.get = function(ns) {\n return this.sources[ns];\n};\n\n/** @deprecated */\nLocusZoom.DataSources.prototype.removeSource = function(ns) {\n console.warn(\"Warning: .removeSource() is deprecated. Use .remove() instead\");\n return this.remove(ns);\n};\n\n/**\n * Remove the datasource associated with a given namespace\n * @public\n * @param {String} ns Namespace\n */\nLocusZoom.DataSources.prototype.remove = function(ns) {\n return this.set(ns, null);\n};\n\n/**\n * Populate a list of datasources specified as a JSON object\n * @public\n * @param {String|Object} x An object or JSON representation containing {ns: configArray} entries\n * @returns {LocusZoom.DataSources}\n */\nLocusZoom.DataSources.prototype.fromJSON = function(x) {\n if (typeof x === \"string\") {\n x = JSON.parse(x);\n }\n var ds = this;\n Object.keys(x).forEach(function(ns) {\n ds.set(ns, x[ns]);\n });\n return ds;\n};\n\n/**\n * Return the names of all currently recognized datasources\n * @public\n * @returns {Array}\n */\nLocusZoom.DataSources.prototype.keys = function() {\n return Object.keys(this.sources);\n};\n\n/**\n * Datasources can be instantiated from a JSON object instead of code. This represents existing sources in that format.\n * For example, this can be helpful when sharing plots, or to share settings with others when debugging\n * @public\n */\nLocusZoom.DataSources.prototype.toJSON = function() {\n return this.sources;\n};\n\n/**\n * Represents an addressable unit of data from a namespaced datasource, subject to specified value transformations.\n *\n * When used by a data layer, fields will automatically be re-fetched from the appropriate data source whenever the\n * state of a plot fetches, eg pan or zoom operations that would affect what data is displayed.\n *\n * @public\n * @class\n * @param {String} field A string representing the namespace of the datasource, the name of the desired field to fetch\n * from that datasource, and arbitrarily many transformations to apply to the value. The namespace and\n * transformation(s) are optional and information is delimited according to the general syntax\n * `[namespace:]name[|transformation][|transformation]`. For example, `association:pvalue|neglog10`\n */\nLocusZoom.Data.Field = function(field){\n \n var parts = /^(?:([^:]+):)?([^:|]*)(\\|.+)*$/.exec(field);\n /** @member {String} */\n this.full_name = field;\n /** @member {String} */\n this.namespace = parts[1] || null;\n /** @member {String} */\n this.name = parts[2] || null;\n /** @member {Array} */\n this.transformations = [];\n \n if (typeof parts[3] == \"string\" && parts[3].length > 1){\n this.transformations = parts[3].substring(1).split(\"|\");\n this.transformations.forEach(function(transform, i){\n this.transformations[i] = LocusZoom.TransformationFunctions.get(transform);\n }.bind(this));\n }\n\n this.applyTransformations = function(val){\n this.transformations.forEach(function(transform){\n val = transform(val);\n });\n return val;\n };\n\n // Resolve the field for a given data element.\n // First look for a full match with transformations already applied by the data requester.\n // Otherwise prefer a namespace match and fall back to just a name match, applying transformations on the fly.\n this.resolve = function(d){\n if (typeof d[this.full_name] == \"undefined\"){\n var val = null;\n if (typeof (d[this.namespace+\":\"+this.name]) != \"undefined\"){ val = d[this.namespace+\":\"+this.name]; }\n else if (typeof d[this.name] != \"undefined\"){ val = d[this.name]; }\n d[this.full_name] = this.applyTransformations(val);\n }\n return d[this.full_name];\n };\n \n};\n\n/**\n * The Requester manages fetching of data across multiple data sources. It is used internally by LocusZoom data layers.\n * It passes state information and ensures that data is formatted in the manner expected by the plot.\n *\n * It is also responsible for constructing a \"chain\" of dependent requests, by requesting each datasource\n * sequentially in the order specified in the datalayer `fields` array. Data sources are only chained within a\n * data layer, and only if that layer requests more than one kind of data source.\n * @param {LocusZoom.DataSources} sources An object of {ns: LocusZoom.Data.Source} instances\n * @class\n */\nLocusZoom.Data.Requester = function(sources) {\n\n function split_requests(fields) {\n // Given a fields array, return an object specifying what datasource names the data layer should make requests\n // to, and how to handle the returned data\n var requests = {};\n // Regular expression finds namespace:field|trans\n var re = /^(?:([^:]+):)?([^:|]*)(\\|.+)*$/;\n fields.forEach(function(raw) {\n var parts = re.exec(raw);\n var ns = parts[1] || \"base\";\n var field = parts[2];\n var trans = LocusZoom.TransformationFunctions.get(parts[3]);\n if (typeof requests[ns] ==\"undefined\") {\n requests[ns] = {outnames:[], fields:[], trans:[]};\n }\n requests[ns].outnames.push(raw);\n requests[ns].fields.push(field);\n requests[ns].trans.push(trans);\n });\n return requests;\n }\n\n /**\n * Fetch data, and create a chain that only connects two data sources if they depend on each other\n * @param {Object} state The current \"state\" of the plot, such as chromosome and start/end positions\n * @param {String[]} fields The list of data fields specified in the `layout` for a specific data layer\n * @returns {Promise}\n */\n this.getData = function(state, fields) {\n var requests = split_requests(fields);\n // Create an array of functions that, when called, will trigger the request to the specified datasource\n var promises = Object.keys(requests).map(function(key) {\n if (!sources.get(key)) {\n throw(\"Datasource for namespace \" + key + \" not found\");\n }\n return sources.get(key).getData(state, requests[key].fields, \n requests[key].outnames, requests[key].trans);\n });\n //assume the fields are requested in dependent order\n //TODO: better manage dependencies\n var ret = Q.when({header:{}, body:{}});\n for(var i=0; i < promises.length; i++) {\n // If a single datalayer uses multiple sources, perform the next request when the previous one completes\n ret = ret.then(promises[i]);\n }\n return ret;\n };\n};\n\n/**\n * Base class for LocusZoom data sources\n * This can be extended with .extend() to create custom data sources\n * @class\n * @public\n */\nLocusZoom.Data.Source = function() {\n /**\n * Whether this source should enable caching\n * @member {Boolean}\n */\n this.enableCache = true;\n /**\n * Whether this data source type is dependent on previous requests- for example, the LD source cannot annotate\n * association data if no data was found for that region.\n * @member {boolean}\n */\n this.dependentSource = false;\n};\n\n/**\n * A default constructor that can be used when creating new data sources\n * @param {String|Object} init Basic configuration- either a url, or a config object\n * @param {String} [init.url] The datasource URL\n * @param {String} [init.params] Initial config params for the datasource\n */\nLocusZoom.Data.Source.prototype.parseInit = function(init) {\n if (typeof init === \"string\") {\n /** @member {String} */\n this.url = init;\n /** @member {String} */\n this.params = {};\n } else {\n this.url = init.url;\n this.params = init.params || {};\n }\n if (!this.url) {\n throw(\"Source not initialized with required URL\");\n }\n\n};\n\n/**\n * Fetch the internal string used to represent this data when cache is used\n * @protected\n * @param state\n * @param chain\n * @param fields\n * @returns {String|undefined}\n */\nLocusZoom.Data.Source.prototype.getCacheKey = function(state, chain, fields) {\n var url = this.getURL && this.getURL(state, chain, fields);\n return url;\n};\n\n/**\n * Fetch data from a remote location\n * @protected\n * @param {Object} state The state of the parent plot\n * @param chain\n * @param fields\n */\nLocusZoom.Data.Source.prototype.fetchRequest = function(state, chain, fields) {\n var url = this.getURL(state, chain, fields);\n return LocusZoom.createCORSPromise(\"GET\", url); \n};\n// TODO: move this.getURL stub into parent class and add documentation; parent should not check for methods known only to children\n\n\n/**\n * TODO Rename to handleRequest (to disambiguate from, say HTTP get requests) and update wiki docs and other references\n * @protected\n */\nLocusZoom.Data.Source.prototype.getRequest = function(state, chain, fields) {\n var req;\n var cacheKey = this.getCacheKey(state, chain, fields);\n if (this.enableCache && typeof(cacheKey) !== \"undefined\" && cacheKey === this._cachedKey) {\n req = Q.when(this._cachedResponse);\n } else {\n req = this.fetchRequest(state, chain, fields);\n if (this.enableCache) {\n req = req.then(function(x) {\n this._cachedKey = cacheKey;\n return this._cachedResponse = x;\n }.bind(this));\n }\n }\n return req;\n};\n\n/**\n * Fetch the data from the specified data source, and format it in a way that can be used by the consuming plot\n * @protected\n * @param {Object} state The current \"state\" of the plot, such as chromosome and start/end positions\n * @param {String[]} fields Array of field names that the plot has requested from this data source. (without the \"namespace\" prefix) TODO: Clarify how this fieldname maps to raw datasource output, and how it differs from outnames\n * @param {String[]} outnames Array describing how the output data should refer to this field. This represents the\n * originally requested field name, including the namespace. This must be an array with the same length as `fields`\n * @param {Function[]} trans The collection of transformation functions to be run on selected fields.\n * This must be an array with the same length as `fields`\n * @returns {function(this:LocusZoom.Data.Source)} A callable operation that can be used as part of the data chain\n */\nLocusZoom.Data.Source.prototype.getData = function(state, fields, outnames, trans) {\n if (this.preGetData) {\n var pre = this.preGetData(state, fields, outnames, trans);\n if(this.pre) {\n state = pre.state || state;\n fields = pre.fields || fields;\n outnames = pre.outnames || outnames;\n trans = pre.trans || trans;\n }\n }\n\n var self = this;\n return function (chain) {\n if (self.dependentSource && chain && chain.body && !chain.body.length) {\n // A \"dependent\" source should not attempt to fire a request if there is no data for it to act on.\n // Therefore, it should simply return the previous data chain.\n return Q.when(chain);\n }\n\n return self.getRequest(state, chain, fields).then(function(resp) {\n return self.parseResponse(resp, chain, fields, outnames, trans);\n });\n };\n};\n\n/**\n * Parse response data. Return an object containing \"header\" (metadata or request parameters) and \"body\"\n * (data to be used for plotting). The response from this request is combined with responses from all other requests\n * in the chain.\n * @public\n * @param {String|Object} resp The raw data associated with the response\n * @param {Object} chain The combined parsed response data from this and all other requests made in the chain\n * @param {String[]} fields Array of field names that the plot has requested from this data source. (without the \"namespace\" prefix) TODO: Clarify how this fieldname maps to raw datasource output, and how it differs from outnames\n * @param {String[]} outnames Array describing how the output data should refer to this field. This represents the\n * originally requested field name, including the namespace. This must be an array with the same length as `fields`\n * @param {Function[]} trans The collection of transformation functions to be run on selected fields.\n * This must be an array with the same length as `fields`\n * @returns {{header: ({}|*), body: {}}}\n */\nLocusZoom.Data.Source.prototype.parseResponse = function(resp, chain, fields, outnames, trans) {\n var json = typeof resp == \"string\" ? JSON.parse(resp) : resp;\n var records = this.parseData(json.data || json, fields, outnames, trans);\n return {header: chain.header || {}, body: records};\n};\n/**\n * Some API endpoints return an object containing several arrays, representing columns of data. Each array should have\n * the same length, and a given array index corresponds to a single row.\n *\n * This gathers column data into an array of objects, each one representing the combined data for a given record.\n * See `parseData` for usage\n *\n * @protected\n * @param {Object} x A response payload object\n * @param {Array} fields\n * @param {Array} outnames\n * @param {Array} trans\n * @returns {Object[]}\n */\nLocusZoom.Data.Source.prototype.parseArraysToObjects = function(x, fields, outnames, trans) {\n //intended for an object of arrays\n //{\"id\":[1,2], \"val\":[5,10]}\n var records = [];\n fields.forEach(function(f, i) {\n if (!(f in x)) {throw \"field \" + f + \" not found in response for \" + outnames[i];}\n });\n // Safeguard: check that arrays are of same length\n var keys = Object.keys(x);\n var N = x[keys[0]].length;\n var sameLength = keys.every(function(key) {\n var item = x[key];\n return item.length === N;\n });\n if (!sameLength) {\n throw this.constructor.SOURCE_NAME + \" expects a response in which all arrays of data are the same length\";\n }\n\n for(var i = 0; i < N; i++) {\n var record = {};\n for(var j=0; j1) {\n if (fields.length!==2 || fields.indexOf(\"isrefvar\")===-1) {\n throw(\"LD does not know how to get all fields: \" + fields.join(\", \"));\n }\n }\n};\n\nLocusZoom.Data.LDSource.prototype.findMergeFields = function(chain) {\n // since LD may be shared across sources with different namespaces\n // we use regex to find columns to join on rather than \n // requiring exact matches\n var exactMatch = function(arr) {return function() {\n var regexes = arguments;\n for(var i=0; i0) {\n var names = Object.keys(chain.body[0]);\n var nameMatch = exactMatch(names);\n dataFields.id = dataFields.id || nameMatch(/\\bvariant\\b/) || nameMatch(/\\bid\\b/);\n dataFields.position = dataFields.position || nameMatch(/\\bposition\\b/i, /\\bpos\\b/i);\n dataFields.pvalue = dataFields.pvalue || nameMatch(/\\bpvalue\\b/i, /\\blog_pvalue\\b/i);\n dataFields._names_ = names;\n }\n return dataFields;\n};\n\nLocusZoom.Data.LDSource.prototype.findRequestedFields = function(fields, outnames) {\n var obj = {};\n for(var i=0; i extremeVal) {\n extremeVal = x[i][pval] * sign;\n extremeIdx = i;\n }\n }\n return extremeIdx;\n };\n\n var refSource = state.ldrefsource || chain.header.ldrefsource || 1;\n var reqFields = this.findRequestedFields(fields);\n var refVar = reqFields.ldin;\n if (refVar === \"state\") {\n refVar = state.ldrefvar || chain.header.ldrefvar || \"best\";\n }\n if (refVar === \"best\") {\n if (!chain.body) {\n throw(\"No association data found to find best pvalue\");\n }\n var keys = this.findMergeFields(chain);\n if (!keys.pvalue || !keys.id) {\n var columns = \"\";\n if (!keys.id){ columns += (columns.length ? \", \" : \"\") + \"id\"; }\n if (!keys.pvalue){ columns += (columns.length ? \", \" : \"\") + \"pvalue\"; }\n throw(\"Unable to find necessary column(s) for merge: \" + columns + \" (available: \" + keys._names_ + \")\");\n }\n refVar = chain.body[findExtremeValue(chain.body, keys.pvalue)][keys.id];\n }\n if (!chain.header) {chain.header = {};}\n chain.header.ldrefvar = refVar;\n return this.url + \"results/?filter=reference eq \" + refSource + \n \" and chromosome2 eq '\" + state.chr + \"'\" + \n \" and position2 ge \" + state.start + \n \" and position2 le \" + state.end + \n \" and variant1 eq '\" + refVar + \"'\" + \n \"&fields=chr,pos,rsquare\";\n};\n\nLocusZoom.Data.LDSource.prototype.parseResponse = function(resp, chain, fields, outnames) {\n var json = JSON.parse(resp);\n var keys = this.findMergeFields(chain);\n var reqFields = this.findRequestedFields(fields, outnames);\n if (!keys.position) {\n throw(\"Unable to find position field for merge: \" + keys._names_);\n }\n var leftJoin = function(left, right, lfield, rfield) {\n var i=0, j=0;\n while (i < left.length && j < right.position2.length) {\n if (left[i][keys.position] === right.position2[j]) {\n left[i][lfield] = right[rfield][j];\n i++;\n j++;\n } else if (left[i][keys.position] < right.position2[j]) {\n i++;\n } else {\n j++;\n }\n }\n };\n var tagRefVariant = function(data, refvar, idfield, outname) {\n for(var i=0; i} */\n this.panels = {};\n /**\n * TODO: This is currently used by external classes that manipulate the parent and may indicate room for a helper method in the api to coordinate boilerplate\n * @protected\n * @member {String[]}\n */\n this.panel_ids_by_y_index = [];\n\n /**\n * Notify each child panel of the plot of changes in panel ordering/ arrangement\n */\n this.applyPanelYIndexesToPanelLayouts = function(){\n this.panel_ids_by_y_index.forEach(function(pid, idx){\n this.panels[pid].layout.y_index = idx;\n }.bind(this));\n };\n\n /**\n * Get the qualified ID pathname for the plot\n * @returns {String}\n */\n this.getBaseId = function(){\n return this.id;\n };\n\n /**\n * Track update operations (reMap) performed on all child panels, and notify the parent plot when complete\n * TODO: Reconsider whether we need to be tracking this as global state outside of context of specific operations\n * @protected\n * @member {Promise[]}\n */\n this.remap_promises = [];\n\n if (typeof layout == \"undefined\"){\n /**\n * The layout is a serializable object used to describe the composition of the Plot\n * If no layout was passed, use the Standard Association Layout\n * Otherwise merge whatever was passed with the Default Layout\n * TODO: Review description; we *always* merge with default layout?\n * @member {Object}\n */\n this.layout = LocusZoom.Layouts.merge({}, LocusZoom.Layouts.get(\"plot\", \"standard_association\"));\n } else {\n this.layout = layout;\n }\n LocusZoom.Layouts.merge(this.layout, LocusZoom.Plot.DefaultLayout);\n\n /**\n * Values in the layout object may change during rendering etc. Retain a copy of the original plot state\n * @member {Object}\n */\n this._base_layout = JSON.parse(JSON.stringify(this.layout));\n\n\n /**\n * Create a shortcut to the state in the layout on the Plot. Tracking in the layout allows the plot to be created\n * with initial state/setup.\n *\n * Tracks state of the plot, eg start and end position\n * @member {Object}\n */\n this.state = this.layout.state;\n\n /** @member {LocusZoom.Data.Requester} */\n this.lzd = new LocusZoom.Data.Requester(datasource);\n\n /**\n * Window.onresize listener (responsive layouts only)\n * TODO: .on appears to return a selection, not a listener? Check logic here\n * https://github.com/d3/d3-selection/blob/00b904b9bcec4dfaf154ae0bbc777b1fc1d7bc08/test/selection/on-test.js#L11\n * @deprecated\n * @member {d3.selection}\n */\n this.window_onresize = null;\n\n /**\n * Known event hooks that the panel can respond to\n * @protected\n * @member {Object}\n */\n this.event_hooks = {\n \"layout_changed\": [],\n \"data_requested\": [],\n \"data_rendered\": [],\n \"element_clicked\": []\n };\n /**\n * There are several events that a LocusZoom plot can \"emit\" when appropriate, and LocusZoom supports registering\n * \"hooks\" for these events which are essentially custom functions intended to fire at certain times.\n *\n * The following plot-level events are currently supported:\n * - `layout_changed` - context: plot - Any aspect of the plot's layout (including dimensions or state) has changed.\n * - `data_requested` - context: plot - A request for new data from any data source used in the plot has been made.\n * - `data_rendered` - context: plot - Data from a request has been received and rendered in the plot.\n * - `element_clicked` - context: element - A data element in any of the plot's data layers has been clicked.\n *\n * To register a hook for any of these events use `plot.on('event_name', function() {})`.\n *\n * There can be arbitrarily many functions registered to the same event. They will be executed in the order they\n * were registered. The this context bound to each event hook function is dependent on the type of event, as\n * denoted above. For example, when data_requested is emitted the context for this in the event hook will be the\n * plot itself, but when element_clicked is emitted the context for this in the event hook will be the element\n * that was clicked.\n *\n * @param {String} event\n * @param {function} hook\n * @returns {LocusZoom.Plot}\n */\n this.on = function(event, hook){\n if (typeof \"event\" != \"string\" || !Array.isArray(this.event_hooks[event])){\n throw(\"Unable to register event hook, invalid event: \" + event.toString());\n }\n if (typeof hook != \"function\"){\n throw(\"Unable to register event hook, invalid hook function passed\");\n }\n this.event_hooks[event].push(hook);\n return this;\n };\n /**\n * Handle running of event hooks when an event is emitted\n * @protected\n * @param {string} event A known event name\n * @param {*} context Controls function execution context (value of `this` for the hook to be fired)\n * @returns {LocusZoom.Plot}\n */\n this.emit = function(event, context){\n if (typeof \"event\" != \"string\" || !Array.isArray(this.event_hooks[event])){\n throw(\"LocusZoom attempted to throw an invalid event: \" + event.toString());\n }\n context = context || this;\n this.event_hooks[event].forEach(function(hookToRun) {\n hookToRun.call(context);\n });\n return this;\n };\n\n /**\n * Get an object with the x and y coordinates of the plot's origin in terms of the entire page\n * Necessary for positioning any HTML elements over the plot\n * @returns {{x: Number, y: Number, width: Number, height: Number}}\n */\n this.getPageOrigin = function(){\n var bounding_client_rect = this.svg.node().getBoundingClientRect();\n var x_offset = document.documentElement.scrollLeft || document.body.scrollLeft;\n var y_offset = document.documentElement.scrollTop || document.body.scrollTop;\n var container = this.svg.node();\n while (container.parentNode !== null){\n container = container.parentNode;\n if (container !== document && d3.select(container).style(\"position\") !== \"static\"){\n x_offset = -1 * container.getBoundingClientRect().left;\n y_offset = -1 * container.getBoundingClientRect().top;\n break;\n }\n }\n return {\n x: x_offset + bounding_client_rect.left,\n y: y_offset + bounding_client_rect.top,\n width: bounding_client_rect.width,\n height: bounding_client_rect.height\n };\n };\n\n /**\n * Get the top and left offset values for the plot's container element (the div that was populated)\n * @returns {{top: number, left: number}}\n */\n this.getContainerOffset = function(){\n var offset = { top: 0, left: 0 };\n var container = this.container.offsetParent || null;\n while (container !== null){\n offset.top += container.offsetTop;\n offset.left += container.offsetLeft;\n container = container.offsetParent || null;\n }\n return offset;\n };\n\n //\n /**\n * Event information describing interaction (e.g. panning and zooming) is stored on the plot\n * TODO: Add/ document details of interaction structure as we expand\n * @member {{panel_id: String, linked_panel_ids: Array, x_linked: *, dragging: *, zooming: *}}\n * @returns {LocusZoom.Plot}\n */\n this.interaction = {};\n\n /**\n * Track whether the target panel can respond to mouse interaction events\n * @param {String} panel_id\n * @returns {boolean}\n */\n this.canInteract = function(panel_id){\n panel_id = panel_id || null;\n if (panel_id){\n return ((typeof this.interaction.panel_id == \"undefined\" || this.interaction.panel_id === panel_id) && !this.loading_data);\n } else {\n return !(this.interaction.dragging || this.interaction.zooming || this.loading_data);\n }\n };\n\n // Initialize the layout\n this.initializeLayout();\n // TODO: Possibly superfluous return from constructor\n return this;\n};\n\n/**\n * Default/ expected configuration parameters for basic plotting; most plots will override\n *\n * @protected\n * @static\n * @type {Object}\n */\nLocusZoom.Plot.DefaultLayout = {\n state: {},\n width: 1,\n height: 1,\n min_width: 1,\n min_height: 1,\n responsive_resize: false,\n aspect_ratio: 1,\n panels: [],\n dashboard: {\n components: []\n },\n panel_boundaries: true,\n mouse_guide: true\n};\n\n/**\n * Helper method to sum the proportional dimensions of panels, a value that's checked often as panels are added/removed\n * @param {('Height'|'Width')} dimension\n * @returns {number}\n */\nLocusZoom.Plot.prototype.sumProportional = function(dimension){\n if (dimension !== \"height\" && dimension !== \"width\"){\n throw (\"Bad dimension value passed to LocusZoom.Plot.prototype.sumProportional\");\n }\n var total = 0;\n for (var id in this.panels){\n // Ensure every panel contributing to the sum has a non-zero proportional dimension\n if (!this.panels[id].layout[\"proportional_\" + dimension]){\n this.panels[id].layout[\"proportional_\" + dimension] = 1 / Object.keys(this.panels).length;\n }\n total += this.panels[id].layout[\"proportional_\" + dimension];\n }\n return total;\n};\n\n/**\n * Resize the plot to fit the bounding container\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.rescaleSVG = function(){\n var clientRect = this.svg.node().getBoundingClientRect();\n this.setDimensions(clientRect.width, clientRect.height);\n return this;\n};\n\n/**\n * Prepare the plot for first use by performing parameter validation, setting up panels, and calculating dimensions\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.initializeLayout = function(){\n\n // Sanity check layout values\n // TODO: Find a way to generally abstract this, maybe into an object that models allowed layout values?\n if (isNaN(this.layout.width) || this.layout.width <= 0){\n throw (\"Plot layout parameter `width` must be a positive number\");\n }\n if (isNaN(this.layout.height) || this.layout.height <= 0){\n throw (\"Plot layout parameter `width` must be a positive number\");\n }\n if (isNaN(this.layout.aspect_ratio) || this.layout.aspect_ratio <= 0){\n throw (\"Plot layout parameter `aspect_ratio` must be a positive number\");\n }\n\n // If this is a responsive layout then set a namespaced/unique onresize event listener on the window\n if (this.layout.responsive_resize){\n this.window_onresize = d3.select(window).on(\"resize.lz-\"+this.id, function(){\n this.rescaleSVG();\n }.bind(this));\n // Forcing one additional setDimensions() call after the page is loaded clears up\n // any disagreements between the initial layout and the loaded responsive container's size\n d3.select(window).on(\"load.lz-\"+this.id, function(){\n this.setDimensions();\n }.bind(this));\n }\n\n // Add panels\n this.layout.panels.forEach(function(panel_layout){\n this.addPanel(panel_layout);\n }.bind(this));\n\n return this;\n};\n\n/**\n * Set the dimensions for a plot, and ensure that panels are sized and positioned correctly.\n *\n * If dimensions are provided, resizes each panel proportionally to match the new plot dimensions. Otherwise,\n * calculates the appropriate plot dimensions based on all panels.\n * @param {Number} [width] If provided and larger than minimum size, set plot to this width\n * @param {Number} [height] If provided and larger than minimum size, set plot to this height\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.setDimensions = function(width, height){\n\n var id;\n\n // Update minimum allowable width and height by aggregating minimums from panels, then apply minimums to containing element.\n var min_width = parseFloat(this.layout.min_width) || 0;\n var min_height = parseFloat(this.layout.min_height) || 0;\n for (id in this.panels){\n min_width = Math.max(min_width, this.panels[id].layout.min_width);\n if (parseFloat(this.panels[id].layout.min_height) > 0 && parseFloat(this.panels[id].layout.proportional_height) > 0){\n min_height = Math.max(min_height, (this.panels[id].layout.min_height / this.panels[id].layout.proportional_height));\n }\n }\n this.layout.min_width = Math.max(min_width, 1);\n this.layout.min_height = Math.max(min_height, 1);\n d3.select(this.svg.node().parentNode).style({\n \"min-width\": this.layout.min_width + \"px\",\n \"min-height\": this.layout.min_height + \"px\"\n });\n\n // If width and height arguments were passed then adjust them against plot minimums if necessary.\n // Then resize the plot and proportionally resize panels to fit inside the new plot dimensions.\n if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0){\n this.layout.width = Math.max(Math.round(+width), this.layout.min_width);\n this.layout.height = Math.max(Math.round(+height), this.layout.min_height);\n this.layout.aspect_ratio = this.layout.width / this.layout.height;\n // Override discrete values if resizing responsively\n if (this.layout.responsive_resize){\n if (this.svg){\n this.layout.width = Math.max(this.svg.node().parentNode.getBoundingClientRect().width, this.layout.min_width);\n }\n this.layout.height = this.layout.width / this.layout.aspect_ratio;\n if (this.layout.height < this.layout.min_height){\n this.layout.height = this.layout.min_height;\n this.layout.width = this.layout.height * this.layout.aspect_ratio;\n }\n }\n // Resize/reposition panels to fit, update proportional origins if necessary\n var y_offset = 0;\n this.panel_ids_by_y_index.forEach(function(panel_id){\n var panel_width = this.layout.width;\n var panel_height = this.panels[panel_id].layout.proportional_height * this.layout.height;\n this.panels[panel_id].setDimensions(panel_width, panel_height);\n this.panels[panel_id].setOrigin(0, y_offset);\n this.panels[panel_id].layout.proportional_origin.x = 0;\n this.panels[panel_id].layout.proportional_origin.y = y_offset / this.layout.height;\n y_offset += panel_height;\n this.panels[panel_id].dashboard.update();\n }.bind(this));\n }\n\n // If width and height arguments were NOT passed (and panels exist) then determine the plot dimensions\n // by making it conform to panel dimensions, assuming panels are already positioned correctly.\n else if (Object.keys(this.panels).length) {\n this.layout.width = 0;\n this.layout.height = 0;\n for (id in this.panels){\n this.layout.width = Math.max(this.panels[id].layout.width, this.layout.width);\n this.layout.height += this.panels[id].layout.height;\n }\n this.layout.width = Math.max(this.layout.width, this.layout.min_width);\n this.layout.height = Math.max(this.layout.height, this.layout.min_height);\n }\n\n // Keep aspect ratio in agreement with dimensions\n this.layout.aspect_ratio = this.layout.width / this.layout.height;\n\n // Apply layout width and height as discrete values or viewbox values\n if (this.svg !== null){\n if (this.layout.responsive_resize){\n this.svg\n .attr(\"viewBox\", \"0 0 \" + this.layout.width + \" \" + this.layout.height)\n .attr(\"preserveAspectRatio\", \"xMinYMin meet\");\n } else {\n this.svg.attr(\"width\", this.layout.width).attr(\"height\", this.layout.height);\n }\n }\n\n // If the plot has been initialized then trigger some necessary render functions\n if (this.initialized){\n this.panel_boundaries.position();\n this.dashboard.update();\n this.curtain.update();\n this.loader.update();\n }\n\n return this.emit(\"layout_changed\");\n};\n\n/**\n * Create a new panel from a layout, and handle the work of initializing and placing the panel on the plot\n * @param {Object} layout\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Plot.prototype.addPanel = function(layout){\n\n // Sanity checks\n if (typeof layout !== \"object\"){\n throw \"Invalid panel layout passed to LocusZoom.Plot.prototype.addPanel()\";\n }\n\n // Create the Panel and set its parent\n var panel = new LocusZoom.Panel(layout, this);\n\n // Store the Panel on the Plot\n this.panels[panel.id] = panel;\n\n // If a discrete y_index was set in the layout then adjust other panel y_index values to accommodate this one\n if (panel.layout.y_index !== null && !isNaN(panel.layout.y_index)\n && this.panel_ids_by_y_index.length > 0){\n // Negative y_index values should count backwards from the end, so convert negatives to appropriate values here\n if (panel.layout.y_index < 0){\n panel.layout.y_index = Math.max(this.panel_ids_by_y_index.length + panel.layout.y_index, 0);\n }\n this.panel_ids_by_y_index.splice(panel.layout.y_index, 0, panel.id);\n this.applyPanelYIndexesToPanelLayouts();\n } else {\n var length = this.panel_ids_by_y_index.push(panel.id);\n this.panels[panel.id].layout.y_index = length - 1;\n }\n\n // Determine if this panel was already in the layout.panels array.\n // If it wasn't, add it. Either way store the layout.panels array index on the panel.\n var layout_idx = null;\n this.layout.panels.forEach(function(panel_layout, idx){\n if (panel_layout.id === panel.id){ layout_idx = idx; }\n });\n if (layout_idx === null){\n layout_idx = this.layout.panels.push(this.panels[panel.id].layout) - 1;\n }\n this.panels[panel.id].layout_idx = layout_idx;\n\n // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space\n if (this.initialized){\n this.positionPanels();\n // Initialize and load data into the new panel\n this.panels[panel.id].initialize();\n this.panels[panel.id].reMap();\n // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip\n // positioning. TODO: make this additional call unnecessary.\n this.setDimensions(this.layout.width, this.layout.height);\n }\n\n return this.panels[panel.id];\n};\n\n\n/**\n * Clear all state, tooltips, and other persisted data associated with one (or all) panel(s) in the plot\n *\n * This is useful when reloading an existing plot with new data, eg \"click for genome region\" links.\n * This is a utility method for custom usage. It is not fired automatically during normal rerender of existing panels\n * @param {String} [panelId] If provided, clear state for only this panel. Otherwise, clear state for all panels.\n * @param {('wipe'|'reset')} [mode='wipe'] Optionally specify how state should be cleared. `wipe` deletes all data\n * and is useful for when the panel is being removed; `reset` is best when the panel will be reused in place.\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.clearPanelData = function(panelId, mode) {\n mode = mode || \"wipe\";\n\n // TODO: Add unit tests for this method\n var panelsList;\n if (panelId) {\n panelsList = [panelId];\n } else {\n panelsList = Object.keys(this.panels);\n }\n var self = this;\n panelsList.forEach(function(pid) {\n self.panels[pid].data_layer_ids_by_z_index.forEach(function(dlid){\n var layer = self.panels[pid].data_layers[dlid];\n layer.destroyAllTooltips();\n\n delete self.layout.state[pid + \".\" + dlid];\n if(mode === \"reset\") {\n layer.setDefaultState();\n }\n });\n });\n return this;\n};\n\n/**\n * Remove the panel from the plot, and clear any state, tooltips, or other visual elements belonging to nested content\n * @param {String} id\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.removePanel = function(id){\n if (!this.panels[id]){\n throw (\"Unable to remove panel, ID not found: \" + id);\n }\n\n // Hide all panel boundaries\n this.panel_boundaries.hide();\n\n // Destroy all tooltips and state vars for all data layers on the panel\n this.clearPanelData(id);\n\n // Remove all panel-level HTML overlay elements\n this.panels[id].loader.hide();\n this.panels[id].dashboard.destroy(true);\n this.panels[id].curtain.hide();\n\n // Remove the svg container for the panel if it exists\n if (this.panels[id].svg.container){\n this.panels[id].svg.container.remove();\n }\n\n // Delete the panel and its presence in the plot layout and state\n this.layout.panels.splice(this.panels[id].layout_idx, 1);\n delete this.panels[id];\n delete this.layout.state[id];\n\n // Update layout_idx values for all remaining panels\n this.layout.panels.forEach(function(panel_layout, idx){\n this.panels[panel_layout.id].layout_idx = idx;\n }.bind(this));\n\n // Remove the panel id from the y_index array\n this.panel_ids_by_y_index.splice(this.panel_ids_by_y_index.indexOf(id), 1);\n this.applyPanelYIndexesToPanelLayouts();\n\n // Call positionPanels() to keep panels from overlapping and ensure filling all available vertical space\n if (this.initialized){\n // Allow the plot to shrink when panels are removed, by forcing it to recalculate min dimensions from scratch\n this.layout.min_height = this._base_layout.min_height;\n this.layout.min_width = this._base_layout.min_width;\n\n this.positionPanels();\n // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip\n // positioning. TODO: make this additional call unnecessary.\n this.setDimensions(this.layout.width, this.layout.height);\n }\n\n return this;\n};\n\n\n/**\n * Automatically position panels based on panel positioning rules and values.\n * Keep panels from overlapping vertically by adjusting origins, and keep the sum of proportional heights at 1.\n *\n * TODO: This logic currently only supports dynamic positioning of panels to prevent overlap in a VERTICAL orientation.\n * Some framework exists for positioning panels in horizontal orientations as well (width, proportional_width, origin.x, etc.)\n * but the logic for keeping these user-definable values straight approaches the complexity of a 2D box-packing algorithm.\n * That's complexity we don't need right now, and may not ever need, so it's on hiatus until a use case materializes.\n */\nLocusZoom.Plot.prototype.positionPanels = function(){\n\n var id;\n\n // We want to enforce that all x-linked panels have consistent horizontal margins\n // (to ensure that aligned items stay aligned despite inconsistent initial layout parameters)\n // NOTE: This assumes panels have consistent widths already. That should probably be enforced too!\n var x_linked_margins = { left: 0, right: 0 };\n\n // Proportional heights for newly added panels default to null unless explicitly set, so determine appropriate\n // proportional heights for all panels with a null value from discretely set dimensions.\n // Likewise handle default nulls for proportional widths, but instead just force a value of 1 (full width)\n for (id in this.panels){\n if (this.panels[id].layout.proportional_height === null){\n this.panels[id].layout.proportional_height = this.panels[id].layout.height / this.layout.height;\n }\n if (this.panels[id].layout.proportional_width === null){\n this.panels[id].layout.proportional_width = 1;\n }\n if (this.panels[id].layout.interaction.x_linked){\n x_linked_margins.left = Math.max(x_linked_margins.left, this.panels[id].layout.margin.left);\n x_linked_margins.right = Math.max(x_linked_margins.right, this.panels[id].layout.margin.right);\n }\n }\n\n // Sum the proportional heights and then adjust all proportionally so that the sum is exactly 1\n var total_proportional_height = this.sumProportional(\"height\");\n if (!total_proportional_height){\n return this;\n }\n var proportional_adjustment = 1 / total_proportional_height;\n for (id in this.panels){\n this.panels[id].layout.proportional_height *= proportional_adjustment;\n }\n\n // Update origins on all panels without changing plot-level dimensions yet\n // Also apply x-linked margins to x-linked panels, updating widths as needed\n var y_offset = 0;\n this.panel_ids_by_y_index.forEach(function(panel_id){\n this.panels[panel_id].setOrigin(0, y_offset);\n this.panels[panel_id].layout.proportional_origin.x = 0;\n y_offset += this.panels[panel_id].layout.height;\n if (this.panels[panel_id].layout.interaction.x_linked){\n var delta = Math.max(x_linked_margins.left - this.panels[panel_id].layout.margin.left, 0)\n + Math.max(x_linked_margins.right - this.panels[panel_id].layout.margin.right, 0);\n this.panels[panel_id].layout.width += delta;\n this.panels[panel_id].layout.margin.left = x_linked_margins.left;\n this.panels[panel_id].layout.margin.right = x_linked_margins.right;\n this.panels[panel_id].layout.cliparea.origin.x = x_linked_margins.left;\n }\n }.bind(this));\n var calculated_plot_height = y_offset;\n this.panel_ids_by_y_index.forEach(function(panel_id){\n this.panels[panel_id].layout.proportional_origin.y = this.panels[panel_id].layout.origin.y / calculated_plot_height;\n }.bind(this));\n\n // Update dimensions on the plot to accommodate repositioned panels\n this.setDimensions();\n\n // Set dimensions on all panels using newly set plot-level dimensions and panel-level proportional dimensions\n this.panel_ids_by_y_index.forEach(function(panel_id){\n this.panels[panel_id].setDimensions(this.layout.width * this.panels[panel_id].layout.proportional_width,\n this.layout.height * this.panels[panel_id].layout.proportional_height);\n }.bind(this));\n\n return this;\n\n};\n\n/**\n * Prepare the first rendering of the plot. This includes initializing the individual panels, but also creates shared\n * elements such as mouse events, panel guides/boundaries, and loader/curtain.\n *\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.initialize = function(){\n\n // Ensure proper responsive class is present on the containing node if called for\n if (this.layout.responsive_resize){\n d3.select(this.container).classed(\"lz-container-responsive\", true);\n }\n\n // Create an element/layer for containing mouse guides\n if (this.layout.mouse_guide) {\n var mouse_guide_svg = this.svg.append(\"g\")\n .attr(\"class\", \"lz-mouse_guide\").attr(\"id\", this.id + \".mouse_guide\");\n var mouse_guide_vertical_svg = mouse_guide_svg.append(\"rect\")\n .attr(\"class\", \"lz-mouse_guide-vertical\").attr(\"x\",-1);\n var mouse_guide_horizontal_svg = mouse_guide_svg.append(\"rect\")\n .attr(\"class\", \"lz-mouse_guide-horizontal\").attr(\"y\",-1);\n this.mouse_guide = {\n svg: mouse_guide_svg,\n vertical: mouse_guide_vertical_svg,\n horizontal: mouse_guide_horizontal_svg\n };\n }\n\n // Add curtain and loader prototpyes to the plot\n this.curtain = LocusZoom.generateCurtain.call(this);\n this.loader = LocusZoom.generateLoader.call(this);\n\n // Create the panel_boundaries object with show/position/hide methods\n this.panel_boundaries = {\n parent: this,\n hide_timeout: null,\n showing: false,\n dragging: false,\n selectors: [],\n corner_selector: null,\n show: function(){\n // Generate panel boundaries\n if (!this.showing && !this.parent.curtain.showing){\n this.showing = true;\n // Loop through all panels to create a horizontal boundary for each\n this.parent.panel_ids_by_y_index.forEach(function(panel_id, panel_idx){\n var selector = d3.select(this.parent.svg.node().parentNode).insert(\"div\", \".lz-data_layer-tooltip\")\n .attr(\"class\", \"lz-panel-boundary\")\n .attr(\"title\", \"Resize panel\");\n selector.append(\"span\");\n var panel_resize_drag = d3.behavior.drag();\n panel_resize_drag.on(\"dragstart\", function(){ this.dragging = true; }.bind(this));\n panel_resize_drag.on(\"dragend\", function(){ this.dragging = false; }.bind(this));\n panel_resize_drag.on(\"drag\", function(){\n // First set the dimensions on the panel we're resizing\n var this_panel = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]];\n var original_panel_height = this_panel.layout.height;\n this_panel.setDimensions(this_panel.layout.width, this_panel.layout.height + d3.event.dy);\n var panel_height_change = this_panel.layout.height - original_panel_height;\n var new_calculated_plot_height = this.parent.layout.height + panel_height_change;\n // Next loop through all panels.\n // Update proportional dimensions for all panels including the one we've resized using discrete heights.\n // Reposition panels with a greater y-index than this panel to their appropriate new origin.\n this.parent.panel_ids_by_y_index.forEach(function(loop_panel_id, loop_panel_idx){\n var loop_panel = this.parent.panels[this.parent.panel_ids_by_y_index[loop_panel_idx]];\n loop_panel.layout.proportional_height = loop_panel.layout.height / new_calculated_plot_height;\n if (loop_panel_idx > panel_idx){\n loop_panel.setOrigin(loop_panel.layout.origin.x, loop_panel.layout.origin.y + panel_height_change);\n loop_panel.dashboard.position();\n }\n }.bind(this));\n // Reset dimensions on the entire plot and reposition panel boundaries\n this.parent.positionPanels();\n this.position();\n }.bind(this));\n selector.call(panel_resize_drag);\n this.parent.panel_boundaries.selectors.push(selector);\n }.bind(this));\n // Create a corner boundary / resize element on the bottom-most panel that resizes the entire plot\n var corner_selector = d3.select(this.parent.svg.node().parentNode).insert(\"div\", \".lz-data_layer-tooltip\")\n .attr(\"class\", \"lz-panel-corner-boundary\")\n .attr(\"title\", \"Resize plot\");\n corner_selector.append(\"span\").attr(\"class\", \"lz-panel-corner-boundary-outer\");\n corner_selector.append(\"span\").attr(\"class\", \"lz-panel-corner-boundary-inner\");\n var corner_drag = d3.behavior.drag();\n corner_drag.on(\"dragstart\", function(){ this.dragging = true; }.bind(this));\n corner_drag.on(\"dragend\", function(){ this.dragging = false; }.bind(this));\n corner_drag.on(\"drag\", function(){\n this.setDimensions(this.layout.width + d3.event.dx, this.layout.height + d3.event.dy);\n }.bind(this.parent));\n corner_selector.call(corner_drag);\n this.parent.panel_boundaries.corner_selector = corner_selector;\n }\n return this.position();\n },\n position: function(){\n if (!this.showing){ return this; }\n // Position panel boundaries\n var plot_page_origin = this.parent.getPageOrigin();\n this.selectors.forEach(function(selector, panel_idx){\n var panel_page_origin = this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].getPageOrigin();\n var left = plot_page_origin.x;\n var top = panel_page_origin.y + this.parent.panels[this.parent.panel_ids_by_y_index[panel_idx]].layout.height - 12;\n var width = this.parent.layout.width - 1;\n selector.style({\n top: top + \"px\",\n left: left + \"px\",\n width: width + \"px\"\n });\n selector.select(\"span\").style({\n width: width + \"px\"\n });\n }.bind(this));\n // Position corner selector\n var corner_padding = 10;\n var corner_size = 16;\n this.corner_selector.style({\n top: (plot_page_origin.y + this.parent.layout.height - corner_padding - corner_size) + \"px\",\n left: (plot_page_origin.x + this.parent.layout.width - corner_padding - corner_size) + \"px\"\n });\n return this;\n },\n hide: function(){\n if (!this.showing){ return this; }\n this.showing = false;\n // Remove panel boundaries\n this.selectors.forEach(function(selector){ selector.remove(); });\n this.selectors = [];\n // Remove corner boundary\n this.corner_selector.remove();\n this.corner_selector = null;\n return this;\n }\n };\n\n // Show panel boundaries stipulated by the layout (basic toggle, only show on mouse over plot)\n if (this.layout.panel_boundaries){\n d3.select(this.svg.node().parentNode).on(\"mouseover.\" + this.id + \".panel_boundaries\", function(){\n clearTimeout(this.panel_boundaries.hide_timeout);\n this.panel_boundaries.show();\n }.bind(this));\n d3.select(this.svg.node().parentNode).on(\"mouseout.\" + this.id + \".panel_boundaries\", function(){\n this.panel_boundaries.hide_timeout = setTimeout(function(){\n this.panel_boundaries.hide();\n }.bind(this), 300);\n }.bind(this));\n }\n\n // Create the dashboard object and immediately show it\n this.dashboard = new LocusZoom.Dashboard(this).show();\n\n // Initialize all panels\n for (var id in this.panels){\n this.panels[id].initialize();\n }\n\n // Define plot-level mouse events\n var namespace = \".\" + this.id;\n if (this.layout.mouse_guide) {\n var mouseout_mouse_guide = function(){\n this.mouse_guide.vertical.attr(\"x\", -1);\n this.mouse_guide.horizontal.attr(\"y\", -1);\n }.bind(this);\n var mousemove_mouse_guide = function(){\n var coords = d3.mouse(this.svg.node());\n this.mouse_guide.vertical.attr(\"x\", coords[0]);\n this.mouse_guide.horizontal.attr(\"y\", coords[1]);\n }.bind(this);\n this.svg\n .on(\"mouseout\" + namespace + \"-mouse_guide\", mouseout_mouse_guide)\n .on(\"touchleave\" + namespace + \"-mouse_guide\", mouseout_mouse_guide)\n .on(\"mousemove\" + namespace + \"-mouse_guide\", mousemove_mouse_guide);\n }\n var mouseup = function(){\n this.stopDrag();\n }.bind(this);\n var mousemove = function(){\n if (this.interaction.dragging){\n var coords = d3.mouse(this.svg.node());\n if (d3.event){ d3.event.preventDefault(); }\n this.interaction.dragging.dragged_x = coords[0] - this.interaction.dragging.start_x;\n this.interaction.dragging.dragged_y = coords[1] - this.interaction.dragging.start_y;\n this.panels[this.interaction.panel_id].render();\n this.interaction.linked_panel_ids.forEach(function(panel_id){\n this.panels[panel_id].render();\n }.bind(this));\n }\n }.bind(this);\n this.svg\n .on(\"mouseup\" + namespace, mouseup)\n .on(\"touchend\" + namespace, mouseup)\n .on(\"mousemove\" + namespace, mousemove)\n .on(\"touchmove\" + namespace, mousemove);\n\n // Add an extra namespaced mouseup handler to the containing body, if there is one\n // This helps to stop interaction events gracefully when dragging outside of the plot element\n if (!d3.select(\"body\").empty()){\n d3.select(\"body\")\n .on(\"mouseup\" + namespace, mouseup)\n .on(\"touchend\" + namespace, mouseup);\n }\n\n this.initialized = true;\n\n // An extra call to setDimensions with existing discrete dimensions fixes some rounding errors with tooltip\n // positioning. TODO: make this additional call unnecessary.\n var client_rect = this.svg.node().getBoundingClientRect();\n var width = client_rect.width ? client_rect.width : this.layout.width;\n var height = client_rect.height ? client_rect.height : this.layout.height;\n this.setDimensions(width, height);\n\n return this;\n\n};\n\n/**\n * Refresh (or fetch) a plot's data from sources, regardless of whether position or state has changed\n * @returns {Promise}\n */\nLocusZoom.Plot.prototype.refresh = function(){\n return this.applyState();\n};\n\n/**\n * Update state values and trigger a pull for fresh data on all data sources for all data layers\n * @param state_changes\n * @returns {Promise} A promise that resolves when all data fetch and update operations are complete\n */\nLocusZoom.Plot.prototype.applyState = function(state_changes){\n\n state_changes = state_changes || {};\n if (typeof state_changes != \"object\"){\n throw(\"LocusZoom.applyState only accepts an object; \" + (typeof state_changes) + \" given\");\n }\n\n // First make a copy of the current (old) state to work with\n var new_state = JSON.parse(JSON.stringify(this.state));\n\n // Apply changes by top-level property to the new state\n for (var property in state_changes) {\n new_state[property] = state_changes[property];\n }\n\n // Validate the new state (may do nothing, may do a lot, depends on how the user has things set up)\n new_state = LocusZoom.validateState(new_state, this.layout);\n\n // Apply new state to the actual state\n for (property in new_state) {\n this.state[property] = new_state[property];\n }\n\n // Generate requests for all panels given new state\n this.emit(\"data_requested\");\n this.remap_promises = [];\n this.loading_data = true;\n for (var id in this.panels){\n this.remap_promises.push(this.panels[id].reMap());\n }\n\n return Q.all(this.remap_promises)\n .catch(function(error){\n console.error(error);\n this.curtain.drop(error);\n this.loading_data = false;\n }.bind(this))\n .then(function(){\n // TODO: Check logic here; in some promise implementations, this would cause the error to be considered handled, and \"then\" would always fire. (may or may not be desired behavior)\n // Update dashboard / components\n this.dashboard.update();\n\n // Apply panel-level state values\n this.panel_ids_by_y_index.forEach(function(panel_id){\n var panel = this.panels[panel_id];\n panel.dashboard.update();\n // Apply data-layer-level state values\n panel.data_layer_ids_by_z_index.forEach(function(data_layer_id){\n var data_layer = this.data_layers[data_layer_id];\n var state_id = panel_id + \".\" + data_layer_id;\n for (var property in this.state[state_id]){\n if (!this.state[state_id].hasOwnProperty(property)){ continue; }\n if (Array.isArray(this.state[state_id][property])){\n this.state[state_id][property].forEach(function(element_id){\n try {\n this.setElementStatus(property, this.getElementById(element_id), true);\n } catch (e){\n console.error(\"Unable to apply state: \" + state_id + \", \" + property);\n }\n }.bind(data_layer));\n }\n }\n }.bind(panel));\n }.bind(this));\n\n // Emit events\n this.emit(\"layout_changed\");\n this.emit(\"data_rendered\");\n\n this.loading_data = false;\n\n }.bind(this));\n};\n\n/**\n * Register interactions along the specified axis, provided that the target panel allows interaction.\n *\n * @param {LocusZoom.Panel} panel\n * @param {('x_tick'|'y1_tick'|'y2_tick')} method The direction (axis) along which dragging is being performed.\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.startDrag = function(panel, method){\n\n panel = panel || null;\n method = method || null;\n\n var axis = null;\n switch (method){\n case \"background\":\n case \"x_tick\":\n axis = \"x\";\n break;\n case \"y1_tick\":\n axis = \"y1\";\n break;\n case \"y2_tick\":\n axis = \"y2\";\n break;\n }\n\n if (!(panel instanceof LocusZoom.Panel) || !axis || !this.canInteract()){ return this.stopDrag(); }\n\n var coords = d3.mouse(this.svg.node());\n this.interaction = {\n panel_id: panel.id,\n linked_panel_ids: panel.getLinkedPanelIds(axis),\n dragging: {\n method: method,\n start_x: coords[0],\n start_y: coords[1],\n dragged_x: 0,\n dragged_y: 0,\n axis: axis\n }\n };\n\n this.svg.style(\"cursor\", \"all-scroll\");\n\n return this;\n\n};\n\n/**\n * Process drag interactions across the target panel and synchronize plot state across other panels in sync;\n * clear the event when complete\n * @returns {LocusZoom.Plot}\n */\nLocusZoom.Plot.prototype.stopDrag = function(){\n\n if (!this.interaction.dragging){ return this; }\n\n if (typeof this.panels[this.interaction.panel_id] != \"object\"){\n this.interaction = {};\n return this;\n }\n var panel = this.panels[this.interaction.panel_id];\n\n // Helper function to find the appropriate axis layouts on child data layers\n // Once found, apply the extent as floor/ceiling and remove all other directives\n // This forces all associated axes to conform to the extent generated by a drag action\n var overrideAxisLayout = function(axis, axis_number, extent){\n panel.data_layer_ids_by_z_index.forEach(function(id){\n if (panel.data_layers[id].layout[axis+\"_axis\"].axis === axis_number){\n panel.data_layers[id].layout[axis+\"_axis\"].floor = extent[0];\n panel.data_layers[id].layout[axis+\"_axis\"].ceiling = extent[1];\n delete panel.data_layers[id].layout[axis+\"_axis\"].lower_buffer;\n delete panel.data_layers[id].layout[axis+\"_axis\"].upper_buffer;\n delete panel.data_layers[id].layout[axis+\"_axis\"].min_extent;\n delete panel.data_layers[id].layout[axis+\"_axis\"].ticks;\n }\n });\n };\n\n switch(this.interaction.dragging.method){\n case \"background\":\n case \"x_tick\":\n if (this.interaction.dragging.dragged_x !== 0){\n overrideAxisLayout(\"x\", 1, panel.x_extent);\n this.applyState({ start: panel.x_extent[0], end: panel.x_extent[1] });\n }\n break;\n case \"y1_tick\":\n case \"y2_tick\":\n if (this.interaction.dragging.dragged_y !== 0){\n // TODO: Hardcoded assumption of only two possible axes with single-digit #s (switch/case)\n var y_axis_number = parseInt(this.interaction.dragging.method[1]);\n overrideAxisLayout(\"y\", y_axis_number, panel[\"y\"+y_axis_number+\"_extent\"]);\n }\n break;\n }\n\n this.interaction = {};\n this.svg.style(\"cursor\", null);\n\n return this;\n\n};\n","/* global LocusZoom */\n\"use strict\";\n\n/**\n * A panel is an abstract class representing a subdivision of the LocusZoom stage\n * to display a distinct data representation as a collection of data layers.\n * @class\n * @param {Object} layout\n * @param {LocusZoom.Plot|null} parent\n*/\nLocusZoom.Panel = function(layout, parent) {\n\n if (typeof layout !== \"object\"){\n throw \"Unable to create panel, invalid layout\";\n }\n\n /** @member {LocusZoom.Plot|null} */\n this.parent = parent || null;\n /** @member {LocusZoom.Plot|null} */\n this.parent_plot = parent;\n\n // Ensure a valid ID is present. If there is no valid ID then generate one\n if (typeof layout.id !== \"string\" || !layout.id.length){\n if (!this.parent){\n layout.id = \"p\" + Math.floor(Math.random()*Math.pow(10,8));\n } else {\n var id = null;\n var generateID = function(){\n id = \"p\" + Math.floor(Math.random()*Math.pow(10,8));\n if (id == null || typeof this.parent.panels[id] != \"undefined\"){\n id = generateID();\n }\n }.bind(this);\n layout.id = id;\n }\n } else if (this.parent) {\n if (typeof this.parent.panels[layout.id] !== \"undefined\"){\n throw \"Cannot create panel with id [\" + layout.id + \"]; panel with that id already exists\";\n }\n }\n /** @member {String} */\n this.id = layout.id;\n\n /** @member {Boolean} */\n this.initialized = false;\n /**\n * The index of this panel in the parent plot's `layout.panels`\n * @member {number}\n * */\n this.layout_idx = null;\n /** @member {Object} */\n this.svg = {};\n\n /**\n * A JSON-serializable object used to describe the composition of the Panel\n * @member {Object}\n */\n this.layout = LocusZoom.Layouts.merge(layout || {}, LocusZoom.Panel.DefaultLayout);\n\n // Define state parameters specific to this panel\n if (this.parent){\n /** @member {Object} */\n this.state = this.parent.state;\n\n /** @member {String} */\n this.state_id = this.id;\n this.state[this.state_id] = this.state[this.state_id] || {};\n } else {\n this.state = null;\n this.state_id = null;\n }\n\n /** @member {Object} */\n this.data_layers = {};\n /** @member {String[]} */\n this.data_layer_ids_by_z_index = [];\n\n /** @protected */\n this.applyDataLayerZIndexesToDataLayerLayouts = function(){\n this.data_layer_ids_by_z_index.forEach(function(dlid, idx){\n this.data_layers[dlid].layout.z_index = idx;\n }.bind(this));\n }.bind(this);\n\n /**\n * Track data requests in progress\n * @member {Promise[]}\n * @protected\n */\n this.data_promises = [];\n\n /** @member {d3.scale} */\n this.x_scale = null;\n /** @member {d3.scale} */\n this.y1_scale = null;\n /** @member {d3.scale} */\n this.y2_scale = null;\n\n /** @member {d3.extent} */\n this.x_extent = null;\n /** @member {d3.extent} */\n this.y1_extent = null;\n /** @member {d3.extent} */\n this.y2_extent = null;\n\n /** @member {Number[]} */\n this.x_ticks = [];\n /** @member {Number[]} */\n this.y1_ticks = [];\n /** @member {Number[]} */\n this.y2_ticks = [];\n\n /**\n * A timeout ID as returned by setTimeout\n * @protected\n * @member {number}\n */\n this.zoom_timeout = null;\n\n /** @returns {string} */\n this.getBaseId = function(){\n return this.parent.id + \".\" + this.id;\n };\n\n /**\n * Known event hooks that the panel can respond to\n * @protected\n * @member {Object}\n */\n this.event_hooks = {\n \"layout_changed\": [],\n \"data_requested\": [],\n \"data_rendered\": [],\n \"element_clicked\": []\n };\n /**\n * There are several events that a LocusZoom panel can \"emit\" when appropriate, and LocusZoom supports registering\n * \"hooks\" for these events which are essentially custom functions intended to fire at certain times.\n *\n * The following panel-level events are currently supported:\n * - `layout_changed` - context: panel - Any aspect of the panel's layout (including dimensions or state) has changed.\n * - `data_requested` - context: panel - A request for new data from any data source used in the panel has been made.\n * - `data_rendered` - context: panel - Data from a request has been received and rendered in the panel.\n * - `element_clicked` - context: element - A data element in any of the panel's data layers has been clicked.\n *\n * To register a hook for any of these events use `panel.on('event_name', function() {})`.\n *\n * There can be arbitrarily many functions registered to the same event. They will be executed in the order they\n * were registered. The this context bound to each event hook function is dependent on the type of event, as\n * denoted above. For example, when data_requested is emitted the context for this in the event hook will be the\n * panel itself, but when element_clicked is emitted the context for this in the event hook will be the element\n * that was clicked.\n *\n * @param {String} event\n * @param {function} hook\n * @returns {LocusZoom.Panel}\n */\n this.on = function(event, hook){\n if (typeof \"event\" != \"string\" || !Array.isArray(this.event_hooks[event])){\n throw(\"Unable to register event hook, invalid event: \" + event.toString());\n }\n if (typeof hook != \"function\"){\n throw(\"Unable to register event hook, invalid hook function passed\");\n }\n this.event_hooks[event].push(hook);\n return this;\n };\n /**\n * Handle running of event hooks when an event is emitted\n * @protected\n * @param {string} event A known event name\n * @param {*} context Controls function execution context (value of `this` for the hook to be fired)\n * @returns {LocusZoom.Panel}\n */\n this.emit = function(event, context){\n if (typeof \"event\" != \"string\" || !Array.isArray(this.event_hooks[event])){\n throw(\"LocusZoom attempted to throw an invalid event: \" + event.toString());\n }\n context = context || this;\n this.event_hooks[event].forEach(function(hookToRun) {\n hookToRun.call(context);\n });\n return this;\n };\n\n /**\n * Get an object with the x and y coordinates of the panel's origin in terms of the entire page\n * Necessary for positioning any HTML elements over the panel\n * @returns {{x: Number, y: Number}}\n */\n this.getPageOrigin = function(){\n var plot_origin = this.parent.getPageOrigin();\n return {\n x: plot_origin.x + this.layout.origin.x,\n y: plot_origin.y + this.layout.origin.y\n };\n };\n\n // Initialize the layout\n this.initializeLayout();\n\n return this;\n\n};\n\n/**\n * Default panel layout\n * @static\n * @type {Object}\n */\nLocusZoom.Panel.DefaultLayout = {\n title: { text: \"\", style: {}, x: 10, y: 22 },\n y_index: null,\n width: 0,\n height: 0,\n origin: { x: 0, y: null },\n min_width: 1,\n min_height: 1,\n proportional_width: null,\n proportional_height: null,\n proportional_origin: { x: 0, y: null },\n margin: { top: 0, right: 0, bottom: 0, left: 0 },\n background_click: \"clear_selections\",\n dashboard: {\n components: []\n },\n cliparea: {\n height: 0,\n width: 0,\n origin: { x: 0, y: 0 }\n },\n axes: { // These are the only axes supported!!\n x: {},\n y1: {},\n y2: {}\n },\n legend: null,\n interaction: {\n drag_background_to_pan: false,\n drag_x_ticks_to_scale: false,\n drag_y1_ticks_to_scale: false,\n drag_y2_ticks_to_scale: false,\n scroll_to_zoom: false,\n x_linked: false,\n y1_linked: false,\n y2_linked: false\n },\n data_layers: []\n};\n\n/**\n * Prepare the panel for first use by performing parameter validation, creating axes, setting default dimensions,\n * and preparing / positioning data layers as appropriate.\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.initializeLayout = function(){\n\n // If the layout is missing BOTH width and proportional width then set the proportional width to 1.\n // This will default the panel to taking up the full width of the plot.\n if (this.layout.width === 0 && this.layout.proportional_width === null){\n this.layout.proportional_width = 1;\n }\n\n // If the layout is missing BOTH height and proportional height then set the proportional height to\n // an equal share of the plot's current height.\n if (this.layout.height === 0 && this.layout.proportional_height === null){\n var panel_count = Object.keys(this.parent.panels).length;\n if (panel_count > 0){\n this.layout.proportional_height = (1 / panel_count);\n } else {\n this.layout.proportional_height = 1;\n }\n }\n\n // Set panel dimensions, origin, and margin\n this.setDimensions();\n this.setOrigin();\n this.setMargin();\n\n // Set ranges\n // TODO: Define stub values in constructor\n this.x_range = [0, this.layout.cliparea.width];\n this.y1_range = [this.layout.cliparea.height, 0];\n this.y2_range = [this.layout.cliparea.height, 0];\n\n // Initialize panel axes\n [\"x\", \"y1\", \"y2\"].forEach(function(axis){\n if (!Object.keys(this.layout.axes[axis]).length || this.layout.axes[axis].render ===false){\n // The default layout sets the axis to an empty object, so set its render boolean here\n this.layout.axes[axis].render = false;\n } else {\n this.layout.axes[axis].render = true;\n this.layout.axes[axis].label = this.layout.axes[axis].label || null;\n this.layout.axes[axis].label_function = this.layout.axes[axis].label_function || null;\n }\n }.bind(this));\n\n // Add data layers (which define x and y extents)\n this.layout.data_layers.forEach(function(data_layer_layout){\n this.addDataLayer(data_layer_layout);\n }.bind(this));\n\n return this;\n\n};\n\n/**\n * Set the dimensions for the panel. If passed with no arguments will calculate optimal size based on layout\n * directives and the available area within the plot. If passed discrete width (number) and height (number) will\n * attempt to resize the panel to them, but may be limited by minimum dimensions defined on the plot or panel.\n *\n * @public\n * @param {number} [width]\n * @param {number} [height]\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.setDimensions = function(width, height){\n if (typeof width != \"undefined\" && typeof height != \"undefined\"){\n if (!isNaN(width) && width >= 0 && !isNaN(height) && height >= 0){\n this.layout.width = Math.max(Math.round(+width), this.layout.min_width);\n this.layout.height = Math.max(Math.round(+height), this.layout.min_height);\n }\n } else {\n if (this.layout.proportional_width !== null){\n this.layout.width = Math.max(this.layout.proportional_width * this.parent.layout.width, this.layout.min_width);\n }\n if (this.layout.proportional_height !== null){\n this.layout.height = Math.max(this.layout.proportional_height * this.parent.layout.height, this.layout.min_height);\n }\n }\n this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0);\n this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0);\n if (this.svg.clipRect){\n this.svg.clipRect.attr(\"width\", this.layout.width).attr(\"height\", this.layout.height);\n }\n if (this.initialized){\n this.render();\n this.curtain.update();\n this.loader.update();\n this.dashboard.update();\n if (this.legend){ this.legend.position(); }\n }\n return this;\n};\n\n/**\n * Set panel origin on the plot, and re-render as appropriate\n *\n * @public\n * @param {number} x\n * @param {number} y\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.setOrigin = function(x, y){\n if (!isNaN(x) && x >= 0){ this.layout.origin.x = Math.max(Math.round(+x), 0); }\n if (!isNaN(y) && y >= 0){ this.layout.origin.y = Math.max(Math.round(+y), 0); }\n if (this.initialized){ this.render(); }\n return this;\n};\n\n/**\n * Set margins around this panel\n * @public\n * @param {number} top\n * @param {number} right\n * @param {number} bottom\n * @param {number} left\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.setMargin = function(top, right, bottom, left){\n var extra;\n if (!isNaN(top) && top >= 0){ this.layout.margin.top = Math.max(Math.round(+top), 0); }\n if (!isNaN(right) && right >= 0){ this.layout.margin.right = Math.max(Math.round(+right), 0); }\n if (!isNaN(bottom) && bottom >= 0){ this.layout.margin.bottom = Math.max(Math.round(+bottom), 0); }\n if (!isNaN(left) && left >= 0){ this.layout.margin.left = Math.max(Math.round(+left), 0); }\n if (this.layout.margin.top + this.layout.margin.bottom > this.layout.height){\n extra = Math.floor(((this.layout.margin.top + this.layout.margin.bottom) - this.layout.height) / 2);\n this.layout.margin.top -= extra;\n this.layout.margin.bottom -= extra;\n }\n if (this.layout.margin.left + this.layout.margin.right > this.layout.width){\n extra = Math.floor(((this.layout.margin.left + this.layout.margin.right) - this.layout.width) / 2);\n this.layout.margin.left -= extra;\n this.layout.margin.right -= extra;\n }\n [\"top\", \"right\", \"bottom\", \"left\"].forEach(function(m){\n this.layout.margin[m] = Math.max(this.layout.margin[m], 0);\n }.bind(this));\n this.layout.cliparea.width = Math.max(this.layout.width - (this.layout.margin.left + this.layout.margin.right), 0);\n this.layout.cliparea.height = Math.max(this.layout.height - (this.layout.margin.top + this.layout.margin.bottom), 0);\n this.layout.cliparea.origin.x = this.layout.margin.left;\n this.layout.cliparea.origin.y = this.layout.margin.top;\n\n if (this.initialized){ this.render(); }\n return this;\n};\n\n/**\n * Set the title for the panel. If passed an object, will merge the object with the existing layout configuration, so\n * that all or only some of the title layout object's parameters can be customized. If passed null, false, or an empty\n * string, the title DOM element will be set to display: none.\n *\n * @param {string|object|null} title The title text, or an object with additional configuration\n * @param {string} title.text Text to display. Since titles are rendered as SVG text, HTML and newlines will not be rendered.\n * @param {number} title.x X-offset, in pixels, for the title's text anchor (default left) relative to the top-left corner of the panel.\n * @param {number} title.y Y-offset, in pixels, for the title's text anchor (default left) relative to the top-left corner of the panel.\n NOTE: SVG y values go from the top down, so the SVG origin of (0,0) is in the top left corner.\n * @param {object} title.style CSS styles object to be applied to the title's DOM element.\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.setTitle = function(title){\n if (typeof this.layout.title == \"string\"){\n var text = this.layout.title;\n this.layout.title = { text: text, x: 0, y: 0, style: {} };\n }\n if (typeof title == \"string\"){\n this.layout.title.text = title;\n } else if (typeof title == \"object\" && title !== null){\n this.layout.title = LocusZoom.Layouts.merge(title, this.layout.title);\n }\n if (this.layout.title.text.length){\n this.title.attr(\"display\", null)\n .attr(\"x\", parseFloat(this.layout.title.x))\n .attr(\"y\", parseFloat(this.layout.title.y))\n .style(this.layout.title.style)\n .text(this.layout.title.text);\n } else {\n this.title.attr(\"display\", \"none\");\n }\n return this;\n};\n\n\n/**\n * Prepare the first rendering of the panel. This includes drawing the individual data layers, but also creates shared\n * elements such as axes, title, and loader/curtain.\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.initialize = function(){\n\n // Append a container group element to house the main panel group element and the clip path\n // Position with initial layout parameters\n this.svg.container = this.parent.svg.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".panel_container\")\n .attr(\"transform\", \"translate(\" + (this.layout.origin.x || 0) + \",\" + (this.layout.origin.y || 0) + \")\");\n\n // Append clip path to the parent svg element, size with initial layout parameters\n var clipPath = this.svg.container.append(\"clipPath\")\n .attr(\"id\", this.getBaseId() + \".clip\");\n this.svg.clipRect = clipPath.append(\"rect\")\n .attr(\"width\", this.layout.width).attr(\"height\", this.layout.height);\n\n // Append svg group for rendering all panel child elements, clipped by the clip path\n this.svg.group = this.svg.container.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".panel\")\n .attr(\"clip-path\", \"url(#\" + this.getBaseId() + \".clip)\");\n\n // Add curtain and loader prototypes to the panel\n /** @member {Object} */\n this.curtain = LocusZoom.generateCurtain.call(this);\n /** @member {Object} */\n this.loader = LocusZoom.generateLoader.call(this);\n\n /**\n * Create the dashboard object and hang components on it as defined by panel layout\n * @member {LocusZoom.Dashboard}\n */\n this.dashboard = new LocusZoom.Dashboard(this);\n\n // Inner border\n this.inner_border = this.svg.group.append(\"rect\")\n .attr(\"class\", \"lz-panel-background\")\n .on(\"click\", function(){\n if (this.layout.background_click === \"clear_selections\"){ this.clearSelections(); }\n }.bind(this));\n\n // Add the title\n /** @member {Element} */\n this.title = this.svg.group.append(\"text\").attr(\"class\", \"lz-panel-title\");\n if (typeof this.layout.title != \"undefined\"){ this.setTitle(); }\n\n // Initialize Axes\n this.svg.x_axis = this.svg.group.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".x_axis\").attr(\"class\", \"lz-x lz-axis\");\n if (this.layout.axes.x.render){\n this.svg.x_axis_label = this.svg.x_axis.append(\"text\")\n .attr(\"class\", \"lz-x lz-axis lz-label\")\n .attr(\"text-anchor\", \"middle\");\n }\n this.svg.y1_axis = this.svg.group.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".y1_axis\").attr(\"class\", \"lz-y lz-y1 lz-axis\");\n if (this.layout.axes.y1.render){\n this.svg.y1_axis_label = this.svg.y1_axis.append(\"text\")\n .attr(\"class\", \"lz-y1 lz-axis lz-label\")\n .attr(\"text-anchor\", \"middle\");\n }\n this.svg.y2_axis = this.svg.group.append(\"g\")\n .attr(\"id\", this.getBaseId() + \".y2_axis\").attr(\"class\", \"lz-y lz-y2 lz-axis\");\n if (this.layout.axes.y2.render){\n this.svg.y2_axis_label = this.svg.y2_axis.append(\"text\")\n .attr(\"class\", \"lz-y2 lz-axis lz-label\")\n .attr(\"text-anchor\", \"middle\");\n }\n\n // Initialize child Data Layers\n this.data_layer_ids_by_z_index.forEach(function(id){\n this.data_layers[id].initialize();\n }.bind(this));\n\n /**\n * Legend object, as defined by panel layout and child data layer layouts\n * @member {LocusZoom.Legend}\n * */\n this.legend = null;\n if (this.layout.legend){\n this.legend = new LocusZoom.Legend(this);\n }\n\n // Establish panel background drag interaction mousedown event handler (on the panel background)\n if (this.layout.interaction.drag_background_to_pan){\n var namespace = \".\" + this.parent.id + \".\" + this.id + \".interaction.drag\";\n var mousedown = function(){\n this.parent.startDrag(this, \"background\");\n }.bind(this);\n this.svg.container.select(\".lz-panel-background\")\n .on(\"mousedown\" + namespace + \".background\", mousedown)\n .on(\"touchstart\" + namespace + \".background\", mousedown);\n }\n\n return this;\n\n};\n\n/**\n * Refresh the sort order of all data layers (called by data layer moveUp and moveDown methods)\n */\nLocusZoom.Panel.prototype.resortDataLayers = function(){\n var sort = [];\n this.data_layer_ids_by_z_index.forEach(function(id){\n sort.push(this.data_layers[id].layout.z_index);\n }.bind(this));\n this.svg.group.selectAll(\"g.lz-data_layer-container\").data(sort).sort(d3.ascending);\n this.applyDataLayerZIndexesToDataLayerLayouts();\n};\n\n/**\n * Get an array of panel IDs that are axis-linked to this panel\n * @param {('x'|'y1'|'y2')} axis\n * @returns {Array}\n */\nLocusZoom.Panel.prototype.getLinkedPanelIds = function(axis){\n axis = axis || null;\n var linked_panel_ids = [];\n if ([\"x\",\"y1\",\"y2\"].indexOf(axis) === -1){ return linked_panel_ids; }\n if (!this.layout.interaction[axis + \"_linked\"]){ return linked_panel_ids; }\n this.parent.panel_ids_by_y_index.forEach(function(panel_id){\n if (panel_id !== this.id && this.parent.panels[panel_id].layout.interaction[axis + \"_linked\"]){\n linked_panel_ids.push(panel_id);\n }\n }.bind(this));\n return linked_panel_ids;\n};\n\n/**\n * Move a panel up relative to others by y-index\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.moveUp = function(){\n if (this.parent.panel_ids_by_y_index[this.layout.y_index - 1]){\n this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index - 1];\n this.parent.panel_ids_by_y_index[this.layout.y_index - 1] = this.id;\n this.parent.applyPanelYIndexesToPanelLayouts();\n this.parent.positionPanels();\n }\n return this;\n};\n\n/**\n * Move a panel down (y-axis) relative to others in the plot\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.moveDown = function(){\n if (this.parent.panel_ids_by_y_index[this.layout.y_index + 1]){\n this.parent.panel_ids_by_y_index[this.layout.y_index] = this.parent.panel_ids_by_y_index[this.layout.y_index + 1];\n this.parent.panel_ids_by_y_index[this.layout.y_index + 1] = this.id;\n this.parent.applyPanelYIndexesToPanelLayouts();\n this.parent.positionPanels();\n }\n return this;\n};\n\n/**\n * Create a new data layer from a provided layout object. Should have the keys specified in `DefaultLayout`\n * Will automatically add at the top (depth/z-index) of the panel unless explicitly directed differently\n * in the layout provided.\n * @param {object} layout\n * @returns {*}\n */\nLocusZoom.Panel.prototype.addDataLayer = function(layout){\n\n // Sanity checks\n if (typeof layout !== \"object\" || typeof layout.id !== \"string\" || !layout.id.length){\n throw \"Invalid data layer layout passed to LocusZoom.Panel.prototype.addDataLayer()\";\n }\n if (typeof this.data_layers[layout.id] !== \"undefined\"){\n throw \"Cannot create data_layer with id [\" + layout.id + \"]; data layer with that id already exists in the panel\";\n }\n if (typeof layout.type !== \"string\"){\n throw \"Invalid data layer type in layout passed to LocusZoom.Panel.prototype.addDataLayer()\";\n }\n\n // If the layout defines a y axis make sure the axis number is set and is 1 or 2 (default to 1)\n if (typeof layout.y_axis == \"object\" && (typeof layout.y_axis.axis == \"undefined\" || [1,2].indexOf(layout.y_axis.axis) === -1)){\n layout.y_axis.axis = 1;\n }\n\n // Create the Data Layer\n var data_layer = LocusZoom.DataLayers.get(layout.type, layout, this);\n\n // Store the Data Layer on the Panel\n this.data_layers[data_layer.id] = data_layer;\n\n // If a discrete z_index was set in the layout then adjust other data layer z_index values to accommodate this one\n if (data_layer.layout.z_index !== null && !isNaN(data_layer.layout.z_index)\n && this.data_layer_ids_by_z_index.length > 0){\n // Negative z_index values should count backwards from the end, so convert negatives to appropriate values here\n if (data_layer.layout.z_index < 0){\n data_layer.layout.z_index = Math.max(this.data_layer_ids_by_z_index.length + data_layer.layout.z_index, 0);\n }\n this.data_layer_ids_by_z_index.splice(data_layer.layout.z_index, 0, data_layer.id);\n this.data_layer_ids_by_z_index.forEach(function(dlid, idx){\n this.data_layers[dlid].layout.z_index = idx;\n }.bind(this));\n } else {\n var length = this.data_layer_ids_by_z_index.push(data_layer.id);\n this.data_layers[data_layer.id].layout.z_index = length - 1;\n }\n\n // Determine if this data layer was already in the layout.data_layers array.\n // If it wasn't, add it. Either way store the layout.data_layers array index on the data_layer.\n var layout_idx = null;\n this.layout.data_layers.forEach(function(data_layer_layout, idx){\n if (data_layer_layout.id === data_layer.id){ layout_idx = idx; }\n });\n if (layout_idx === null){\n layout_idx = this.layout.data_layers.push(this.data_layers[data_layer.id].layout) - 1;\n }\n this.data_layers[data_layer.id].layout_idx = layout_idx;\n\n return this.data_layers[data_layer.id];\n};\n\n/**\n * Remove a data layer by id\n * @param {string} id\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.removeDataLayer = function(id){\n if (!this.data_layers[id]){\n throw (\"Unable to remove data layer, ID not found: \" + id);\n }\n\n // Destroy all tooltips for the data layer\n this.data_layers[id].destroyAllTooltips();\n\n // Remove the svg container for the data layer if it exists\n if (this.data_layers[id].svg.container){\n this.data_layers[id].svg.container.remove();\n }\n\n // Delete the data layer and its presence in the panel layout and state\n this.layout.data_layers.splice(this.data_layers[id].layout_idx, 1);\n delete this.state[this.data_layers[id].state_id];\n delete this.data_layers[id];\n\n // Remove the data_layer id from the z_index array\n this.data_layer_ids_by_z_index.splice(this.data_layer_ids_by_z_index.indexOf(id), 1);\n\n // Update layout_idx and layout.z_index values for all remaining data_layers\n this.applyDataLayerZIndexesToDataLayerLayouts();\n this.layout.data_layers.forEach(function(data_layer_layout, idx){\n this.data_layers[data_layer_layout.id].layout_idx = idx;\n }.bind(this));\n\n return this;\n};\n\n/**\n * Clear all selections on all data layers\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.clearSelections = function(){\n this.data_layer_ids_by_z_index.forEach(function(id){\n this.data_layers[id].setAllElementStatus(\"selected\", false);\n }.bind(this));\n return this;\n};\n\n/**\n * When the parent plot changes state, adjust the panel accordingly. For example, this may include fetching new data\n * from the API as the viewing region changes\n * @returns {Promise}\n */\nLocusZoom.Panel.prototype.reMap = function(){\n this.emit(\"data_requested\");\n this.data_promises = [];\n\n // Remove any previous error messages before attempting to load new data\n this.curtain.hide();\n // Trigger reMap on each Data Layer\n for (var id in this.data_layers){\n try {\n this.data_promises.push(this.data_layers[id].reMap());\n } catch (error) {\n console.warn(error);\n this.curtain.show(error);\n }\n }\n // When all finished trigger a render\n return Q.all(this.data_promises)\n .then(function(){\n this.initialized = true;\n this.render();\n this.emit(\"layout_changed\");\n this.parent.emit(\"layout_changed\");\n this.emit(\"data_rendered\");\n }.bind(this))\n .catch(function(error){\n console.warn(error);\n this.curtain.show(error);\n }.bind(this));\n};\n\n/**\n * Iterate over data layers to generate panel axis extents\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.generateExtents = function(){\n\n // Reset extents\n [\"x\", \"y1\", \"y2\"].forEach(function(axis){\n this[axis + \"_extent\"] = null;\n }.bind(this));\n\n // Loop through the data layers\n for (var id in this.data_layers){\n\n var data_layer = this.data_layers[id];\n\n // If defined and not decoupled, merge the x extent of the data layer with the panel's x extent\n if (data_layer.layout.x_axis && !data_layer.layout.x_axis.decoupled){\n this.x_extent = d3.extent((this.x_extent || []).concat(data_layer.getAxisExtent(\"x\")));\n }\n\n // If defined and not decoupled, merge the y extent of the data layer with the panel's appropriate y extent\n if (data_layer.layout.y_axis && !data_layer.layout.y_axis.decoupled){\n var y_axis = \"y\" + data_layer.layout.y_axis.axis;\n this[y_axis+\"_extent\"] = d3.extent((this[y_axis+\"_extent\"] || []).concat(data_layer.getAxisExtent(\"y\")));\n }\n\n }\n\n // Override x_extent from state if explicitly defined to do so\n if (this.layout.axes.x && this.layout.axes.x.extent === \"state\"){\n this.x_extent = [ this.state.start, this.state.end ];\n }\n\n return this;\n\n};\n\n/**\n * Generate an array of ticks for an axis. These ticks are generated in one of three ways (highest wins):\n * 1. An array of specific tick marks\n * 2. Query each data layer for what ticks are appropriate, and allow a panel-level tick configuration parameter\n * object to override the layer's default presentation settings\n * 3. Generate generic tick marks based on the extent of the data\n * @param {('x'|'y1'|'y2')} axis The string identifier of the axis\n * @returns {Number[]|Object[]} TODO: number format?\n * An array of numbers: interpreted as an array of axis value offsets for positioning.\n * An array of objects: each object must have an 'x' attribute to position the tick.\n * Other supported object keys:\n * * text: string to render for a given tick\n * * style: d3-compatible CSS style object\n * * transform: SVG transform attribute string\n * * color: string or LocusZoom scalable parameter object\n */\nLocusZoom.Panel.prototype.generateTicks = function(axis){\n\n // Parse an explicit 'ticks' attribute in the axis layout\n if (this.layout.axes[axis].ticks){\n var layout = this.layout.axes[axis];\n\n var baseTickConfig = layout.ticks;\n if (Array.isArray(baseTickConfig)){\n // Array of specific ticks hard-coded into a panel will override any ticks that an individual layer might specify\n return baseTickConfig;\n }\n\n if (typeof baseTickConfig === \"object\") {\n // If the layout specifies base configuration for ticks- but without specific positions- then ask each\n // data layer to report the tick marks that it thinks it needs\n // TODO: Few layers currently need to specify custom ticks (which is ok!). But if it becomes common, consider adding mechanisms to deduplicate ticks across layers\n var self = this;\n\n // Pass any layer-specific customizations for how ticks are calculated. (styles are overridden separately)\n var config = { position: baseTickConfig.position };\n\n var combinedTicks = this.data_layer_ids_by_z_index.reduce(function(acc, data_layer_id) {\n var nextLayer = self.data_layers[data_layer_id];\n return acc.concat(nextLayer.getTicks(axis, config));\n }, []);\n\n return combinedTicks.map(function(item) {\n // The layer makes suggestions, but tick configuration params specified on the panel take precedence\n var itemConfig = {};\n itemConfig = LocusZoom.Layouts.merge(itemConfig, baseTickConfig);\n return LocusZoom.Layouts.merge(itemConfig, item);\n });\n }\n }\n\n // If no other configuration is provided, attempt to generate ticks from the extent\n if (this[axis + \"_extent\"]) {\n return LocusZoom.prettyTicks(this[axis + \"_extent\"], \"both\");\n }\n return [];\n};\n\n/**\n * Update rendering of this panel whenever an event triggers a redraw. Assumes that the panel has already been\n * prepared the first time via `initialize`\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.render = function(){\n\n // Position the panel container\n this.svg.container.attr(\"transform\", \"translate(\" + this.layout.origin.x + \",\" + this.layout.origin.y + \")\");\n\n // Set size on the clip rect\n this.svg.clipRect.attr(\"width\", this.layout.width).attr(\"height\", this.layout.height);\n\n // Set and position the inner border, style if necessary\n this.inner_border\n .attr(\"x\", this.layout.margin.left).attr(\"y\", this.layout.margin.top)\n .attr(\"width\", this.layout.width - (this.layout.margin.left + this.layout.margin.right))\n .attr(\"height\", this.layout.height - (this.layout.margin.top + this.layout.margin.bottom));\n if (this.layout.inner_border){\n this.inner_border.style({ \"stroke-width\": 1, \"stroke\": this.layout.inner_border });\n }\n\n // Set/update panel title if necessary\n this.setTitle();\n\n // Regenerate all extents\n this.generateExtents();\n\n // Helper function to constrain any procedurally generated vectors (e.g. ranges, extents)\n // Constraints applied here keep vectors from going to infinity or beyond a definable power of ten\n var constrain = function(value, limit_exponent){\n var neg_min = Math.pow(-10, limit_exponent);\n var neg_max = Math.pow(-10, -limit_exponent);\n var pos_min = Math.pow(10, -limit_exponent);\n var pos_max = Math.pow(10, limit_exponent);\n if (value === Infinity){ value = pos_max; }\n if (value === -Infinity){ value = neg_min; }\n if (value === 0){ value = pos_min; }\n if (value > 0){ value = Math.max(Math.min(value, pos_max), pos_min); }\n if (value < 0){ value = Math.max(Math.min(value, neg_max), neg_min); }\n return value;\n };\n\n // Define default and shifted ranges for all axes\n var ranges = {};\n if (this.x_extent){\n var base_x_range = { start: 0, end: this.layout.cliparea.width };\n if (this.layout.axes.x.range){\n base_x_range.start = this.layout.axes.x.range.start || base_x_range.start;\n base_x_range.end = this.layout.axes.x.range.end || base_x_range.end;\n }\n ranges.x = [base_x_range.start, base_x_range.end];\n ranges.x_shifted = [base_x_range.start, base_x_range.end];\n }\n if (this.y1_extent){\n var base_y1_range = { start: this.layout.cliparea.height, end: 0 };\n if (this.layout.axes.y1.range){\n base_y1_range.start = this.layout.axes.y1.range.start || base_y1_range.start;\n base_y1_range.end = this.layout.axes.y1.range.end || base_y1_range.end;\n }\n ranges.y1 = [base_y1_range.start, base_y1_range.end];\n ranges.y1_shifted = [base_y1_range.start, base_y1_range.end];\n }\n if (this.y2_extent){\n var base_y2_range = { start: this.layout.cliparea.height, end: 0 };\n if (this.layout.axes.y2.range){\n base_y2_range.start = this.layout.axes.y2.range.start || base_y2_range.start;\n base_y2_range.end = this.layout.axes.y2.range.end || base_y2_range.end;\n }\n ranges.y2 = [base_y2_range.start, base_y2_range.end];\n ranges.y2_shifted = [base_y2_range.start, base_y2_range.end];\n }\n\n // Shift ranges based on any drag or zoom interactions currently underway\n if (this.parent.interaction.panel_id && (this.parent.interaction.panel_id === this.id || this.parent.interaction.linked_panel_ids.indexOf(this.id) !== -1)){\n var anchor, scalar = null;\n if (this.parent.interaction.zooming && typeof this.x_scale == \"function\"){\n var current_extent_size = Math.abs(this.x_extent[1] - this.x_extent[0]);\n var current_scaled_extent_size = Math.round(this.x_scale.invert(ranges.x_shifted[1])) - Math.round(this.x_scale.invert(ranges.x_shifted[0]));\n var zoom_factor = this.parent.interaction.zooming.scale;\n var potential_extent_size = Math.floor(current_scaled_extent_size * (1 / zoom_factor));\n if (zoom_factor < 1 && !isNaN(this.parent.layout.max_region_scale)){\n zoom_factor = 1 /(Math.min(potential_extent_size, this.parent.layout.max_region_scale) / current_scaled_extent_size);\n } else if (zoom_factor > 1 && !isNaN(this.parent.layout.min_region_scale)){\n zoom_factor = 1 / (Math.max(potential_extent_size, this.parent.layout.min_region_scale) / current_scaled_extent_size);\n }\n var new_extent_size = Math.floor(current_extent_size * zoom_factor);\n anchor = this.parent.interaction.zooming.center - this.layout.margin.left - this.layout.origin.x;\n var offset_ratio = anchor / this.layout.cliparea.width;\n var new_x_extent_start = Math.max(Math.floor(this.x_scale.invert(ranges.x_shifted[0]) - ((new_extent_size - current_scaled_extent_size) * offset_ratio)), 1);\n ranges.x_shifted = [ this.x_scale(new_x_extent_start), this.x_scale(new_x_extent_start + new_extent_size) ];\n } else if (this.parent.interaction.dragging){\n switch (this.parent.interaction.dragging.method){\n case \"background\":\n ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x;\n ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x;\n break;\n case \"x_tick\":\n if (d3.event && d3.event.shiftKey){\n ranges.x_shifted[0] = +this.parent.interaction.dragging.dragged_x;\n ranges.x_shifted[1] = this.layout.cliparea.width + this.parent.interaction.dragging.dragged_x;\n } else {\n anchor = this.parent.interaction.dragging.start_x - this.layout.margin.left - this.layout.origin.x;\n scalar = constrain(anchor / (anchor + this.parent.interaction.dragging.dragged_x), 3);\n ranges.x_shifted[0] = 0;\n ranges.x_shifted[1] = Math.max(this.layout.cliparea.width * (1 / scalar), 1);\n }\n break;\n case \"y1_tick\":\n case \"y2_tick\":\n var y_shifted = \"y\" + this.parent.interaction.dragging.method[1] + \"_shifted\";\n if (d3.event && d3.event.shiftKey){\n ranges[y_shifted][0] = this.layout.cliparea.height + this.parent.interaction.dragging.dragged_y;\n ranges[y_shifted][1] = +this.parent.interaction.dragging.dragged_y;\n } else {\n anchor = this.layout.cliparea.height - (this.parent.interaction.dragging.start_y - this.layout.margin.top - this.layout.origin.y);\n scalar = constrain(anchor / (anchor - this.parent.interaction.dragging.dragged_y), 3);\n ranges[y_shifted][0] = this.layout.cliparea.height;\n ranges[y_shifted][1] = this.layout.cliparea.height - (this.layout.cliparea.height * (1 / scalar));\n }\n }\n }\n }\n\n // Generate scales and ticks for all axes, then render them\n [\"x\", \"y1\", \"y2\"].forEach(function(axis){\n if (!this[axis + \"_extent\"]){ return; }\n\n // Base Scale\n this[axis + \"_scale\"] = d3.scale.linear()\n .domain(this[axis + \"_extent\"])\n .range(ranges[axis + \"_shifted\"]);\n\n // Shift the extent\n this[axis + \"_extent\"] = [\n this[axis + \"_scale\"].invert(ranges[axis][0]),\n this[axis + \"_scale\"].invert(ranges[axis][1])\n ];\n\n // Finalize Scale\n this[axis + \"_scale\"] = d3.scale.linear()\n .domain(this[axis + \"_extent\"]).range(ranges[axis]);\n\n // Render axis (and generate ticks as needed)\n this.renderAxis(axis);\n }.bind(this));\n\n // Establish mousewheel zoom event handers on the panel (namespacing not passed through by d3, so not used here)\n if (this.layout.interaction.scroll_to_zoom){\n var zoom_handler = function(){\n // Look for a shift key press while scrolling to execute.\n // If not present, gracefully raise a notification and allow conventional scrolling\n if (!d3.event.shiftKey){\n if (this.parent.canInteract(this.id)){\n this.loader.show(\"Press [SHIFT] while scrolling to zoom\").hide(1000);\n }\n return;\n }\n d3.event.preventDefault();\n if (!this.parent.canInteract(this.id)){ return; }\n var coords = d3.mouse(this.svg.container.node());\n var delta = Math.max(-1, Math.min(1, (d3.event.wheelDelta || -d3.event.detail || -d3.event.deltaY)));\n if (delta === 0){ return; }\n this.parent.interaction = {\n panel_id: this.id,\n linked_panel_ids: this.getLinkedPanelIds(\"x\"),\n zooming: {\n scale: (delta < 1) ? 0.9 : 1.1,\n center: coords[0]\n }\n };\n this.render();\n this.parent.interaction.linked_panel_ids.forEach(function(panel_id){\n this.parent.panels[panel_id].render();\n }.bind(this));\n if (this.zoom_timeout !== null){ clearTimeout(this.zoom_timeout); }\n this.zoom_timeout = setTimeout(function(){\n this.parent.interaction = {};\n this.parent.applyState({ start: this.x_extent[0], end: this.x_extent[1] });\n }.bind(this), 500);\n }.bind(this);\n this.zoom_listener = d3.behavior.zoom();\n this.svg.container.call(this.zoom_listener)\n .on(\"wheel.zoom\", zoom_handler)\n .on(\"mousewheel.zoom\", zoom_handler)\n .on(\"DOMMouseScroll.zoom\", zoom_handler);\n }\n\n // Render data layers in order by z-index\n this.data_layer_ids_by_z_index.forEach(function(data_layer_id){\n this.data_layers[data_layer_id].draw().render();\n }.bind(this));\n\n return this;\n};\n\n\n/**\n * Render ticks for a particular axis\n * @param {('x'|'y1'|'y2')} axis The identifier of the axes\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.renderAxis = function(axis){\n\n if ([\"x\", \"y1\", \"y2\"].indexOf(axis) === -1){\n throw(\"Unable to render axis; invalid axis identifier: \" + axis);\n }\n\n var canRender = this.layout.axes[axis].render\n && typeof this[axis + \"_scale\"] == \"function\"\n && !isNaN(this[axis + \"_scale\"](0));\n\n // If the axis has already been rendered then check if we can/can't render it\n // Make sure the axis element is shown/hidden to suit\n if (this[axis+\"_axis\"]){\n this.svg.container.select(\"g.lz-axis.lz-\"+axis).style(\"display\", canRender ? null : \"none\");\n }\n\n if (!canRender){ return this; }\n\n // Axis-specific values to plug in where needed\n var axis_params = {\n x: {\n position: \"translate(\" + this.layout.margin.left + \",\" + (this.layout.height - this.layout.margin.bottom) + \")\",\n orientation: \"bottom\",\n label_x: this.layout.cliparea.width / 2,\n label_y: (this.layout.axes[axis].label_offset || 0),\n label_rotate: null\n },\n y1: {\n position: \"translate(\" + this.layout.margin.left + \",\" + this.layout.margin.top + \")\",\n orientation: \"left\",\n label_x: -1 * (this.layout.axes[axis].label_offset || 0),\n label_y: this.layout.cliparea.height / 2,\n label_rotate: -90\n },\n y2: {\n position: \"translate(\" + (this.layout.width - this.layout.margin.right) + \",\" + this.layout.margin.top + \")\",\n orientation: \"right\",\n label_x: (this.layout.axes[axis].label_offset || 0),\n label_y: this.layout.cliparea.height / 2,\n label_rotate: -90\n }\n };\n\n // Generate Ticks\n this[axis + \"_ticks\"] = this.generateTicks(axis);\n\n // Determine if the ticks are all numbers (d3-automated tick rendering) or not (manual tick rendering)\n var ticksAreAllNumbers = (function(ticks){\n for (var i = 0; i < ticks.length; i++){\n if (isNaN(ticks[i])){\n return false;\n }\n }\n return true;\n })(this[axis+\"_ticks\"]);\n\n // Initialize the axis; set scale and orientation\n this[axis+\"_axis\"] = d3.svg.axis().scale(this[axis+\"_scale\"]).orient(axis_params[axis].orientation).tickPadding(3);\n\n // Set tick values and format\n if (ticksAreAllNumbers){\n this[axis+\"_axis\"].tickValues(this[axis+\"_ticks\"]);\n if (this.layout.axes[axis].tick_format === \"region\"){\n this[axis+\"_axis\"].tickFormat(function(d) { return LocusZoom.positionIntToString(d, 6); });\n }\n } else {\n var ticks = this[axis+\"_ticks\"].map(function(t){\n return(t[axis.substr(0,1)]);\n });\n this[axis+\"_axis\"].tickValues(ticks)\n .tickFormat(function(t, i) { return this[axis+\"_ticks\"][i].text; }.bind(this));\n }\n\n // Position the axis in the SVG and apply the axis construct\n this.svg[axis+\"_axis\"]\n .attr(\"transform\", axis_params[axis].position)\n .call(this[axis+\"_axis\"]);\n\n // If necessary manually apply styles and transforms to ticks as specified by the layout\n if (!ticksAreAllNumbers){\n var tick_selector = d3.selectAll(\"g#\" + this.getBaseId().replace(\".\",\"\\\\.\") + \"\\\\.\" + axis + \"_axis g.tick\");\n var panel = this;\n tick_selector.each(function(d, i){\n var selector = d3.select(this).select(\"text\");\n if (panel[axis+\"_ticks\"][i].style){\n selector.style(panel[axis+\"_ticks\"][i].style);\n }\n if (panel[axis+\"_ticks\"][i].transform){\n selector.attr(\"transform\", panel[axis+\"_ticks\"][i].transform);\n }\n });\n }\n\n // Render the axis label if necessary\n var label = this.layout.axes[axis].label || null;\n if (label !== null){\n this.svg[axis+\"_axis_label\"]\n .attr(\"x\", axis_params[axis].label_x).attr(\"y\", axis_params[axis].label_y)\n .text(LocusZoom.parseFields(this.state, label));\n if (axis_params[axis].label_rotate !== null){\n this.svg[axis+\"_axis_label\"]\n .attr(\"transform\", \"rotate(\" + axis_params[axis].label_rotate + \" \" + axis_params[axis].label_x + \",\" + axis_params[axis].label_y + \")\");\n }\n }\n\n // Attach interactive handlers to ticks as needed\n [\"x\", \"y1\", \"y2\"].forEach(function(axis){\n if (this.layout.interaction[\"drag_\" + axis + \"_ticks_to_scale\"]){\n var namespace = \".\" + this.parent.id + \".\" + this.id + \".interaction.drag\";\n var tick_mouseover = function(){\n if (typeof d3.select(this).node().focus == \"function\"){ d3.select(this).node().focus(); }\n var cursor = (axis === \"x\") ? \"ew-resize\" : \"ns-resize\";\n if (d3.event && d3.event.shiftKey){ cursor = \"move\"; }\n d3.select(this)\n .style({\"font-weight\": \"bold\", \"cursor\": cursor})\n .on(\"keydown\" + namespace, tick_mouseover)\n .on(\"keyup\" + namespace, tick_mouseover);\n };\n this.svg.container.selectAll(\".lz-axis.lz-\" + axis + \" .tick text\")\n .attr(\"tabindex\", 0) // necessary to make the tick focusable so keypress events can be captured\n .on(\"mouseover\" + namespace, tick_mouseover)\n .on(\"mouseout\" + namespace, function(){\n d3.select(this).style({\"font-weight\": \"normal\"});\n d3.select(this).on(\"keydown\" + namespace, null).on(\"keyup\" + namespace, null);\n })\n .on(\"mousedown\" + namespace, function(){\n this.parent.startDrag(this, axis + \"_tick\");\n }.bind(this));\n }\n }.bind(this));\n\n return this;\n\n};\n\n/**\n * Force the height of this panel to the largest absolute height of the data in\n * all child data layers (if not null for any child data layers)\n * @param {number} [target_height] A target height, which will be used in situations when the expected height can be\n * pre-calculated (eg when the layers are transitioning)\n */\nLocusZoom.Panel.prototype.scaleHeightToData = function(target_height){\n target_height = +target_height || null;\n if (target_height === null){\n this.data_layer_ids_by_z_index.forEach(function(id){\n var dh = this.data_layers[id].getAbsoluteDataHeight();\n if (+dh){\n if (target_height === null){ target_height = +dh; }\n else { target_height = Math.max(target_height, +dh); }\n }\n }.bind(this));\n }\n if (+target_height){\n target_height += +this.layout.margin.top + +this.layout.margin.bottom;\n this.setDimensions(this.layout.width, target_height);\n this.parent.setDimensions();\n this.parent.panel_ids_by_y_index.forEach(function(id){\n this.parent.panels[id].layout.proportional_height = null;\n }.bind(this));\n this.parent.positionPanels();\n }\n};\n\n/**\n * Methods to set/unset element statuses across all data layers\n * @param {String} status\n * @param {Boolean} toggle\n * @param {Array} filters\n * @param {Boolean} exclusive\n */\nLocusZoom.Panel.prototype.setElementStatusByFilters = function(status, toggle, filters, exclusive){\n this.data_layer_ids_by_z_index.forEach(function(id){\n this.data_layers[id].setElementStatusByFilters(status, toggle, filters, exclusive);\n }.bind(this));\n};\n/**\n * Set/unset element statuses across all data layers\n * @param {String} status\n * @param {Boolean} toggle\n */\nLocusZoom.Panel.prototype.setAllElementStatus = function(status, toggle){\n this.data_layer_ids_by_z_index.forEach(function(id){\n this.data_layers[id].setAllElementStatus(status, toggle);\n }.bind(this));\n};\n// TODO: Capture documentation for dynamically generated methods\nLocusZoom.DataLayer.Statuses.verbs.forEach(function(verb, idx){\n var adjective = LocusZoom.DataLayer.Statuses.adjectives[idx];\n var antiverb = \"un\" + verb;\n // Set/unset status for arbitrarily many elements given a set of filters\n LocusZoom.Panel.prototype[verb + \"ElementsByFilters\"] = function(filters, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n return this.setElementStatusByFilters(adjective, true, filters, exclusive);\n };\n LocusZoom.Panel.prototype[antiverb + \"ElementsByFilters\"] = function(filters, exclusive){\n if (typeof exclusive == \"undefined\"){ exclusive = false; } else { exclusive = !!exclusive; }\n return this.setElementStatusByFilters(adjective, false, filters, exclusive);\n };\n // Set/unset status for all elements\n LocusZoom.Panel.prototype[verb + \"AllElements\"] = function(){\n this.setAllElementStatus(adjective, true);\n return this;\n };\n LocusZoom.Panel.prototype[antiverb + \"AllElements\"] = function(){\n this.setAllElementStatus(adjective, false);\n return this;\n };\n});\n\n\n/**\n * Add a \"basic\" loader to a panel\n * This method is just a shortcut for adding the most commonly used type of loading indicator, which appears when\n * data is requested, animates (e.g. shows an infinitely cycling progress bar as opposed to one that loads from\n * 0-100% based on actual load progress), and disappears when new data is loaded and rendered.\n *\n *\n * @param {Boolean} show_immediately\n * @returns {LocusZoom.Panel}\n */\nLocusZoom.Panel.prototype.addBasicLoader = function(show_immediately){\n if (typeof show_immediately != \"undefined\"){ show_immediately = true; }\n if (show_immediately){\n this.loader.show(\"Loading...\").animate();\n }\n this.on(\"data_requested\", function(){\n this.loader.show(\"Loading...\").animate();\n }.bind(this));\n this.on(\"data_rendered\", function(){\n this.loader.hide();\n }.bind(this));\n return this;\n};\n"]} \ No newline at end of file diff --git a/dist/locuszoom.css b/dist/locuszoom.css index 7ee347ae..8018c848 100644 --- a/dist/locuszoom.css +++ b/dist/locuszoom.css @@ -43,29 +43,29 @@ svg.lz-locuszoom { stroke: rgb(24, 24, 24); stroke-opacity: 1; stroke-width: 1px; } - svg.lz-locuszoom path.lz-data_layer-scatter { + svg.lz-locuszoom path.lz-data_layer-scatter, svg.lz-locuszoom path.lz-data_layer-category_scatter { stroke: rgb(24, 24, 24); stroke-opacity: 0.4; stroke-width: 1px; cursor: pointer; } - svg.lz-locuszoom path.lz-data_layer-scatter-highlighted { + svg.lz-locuszoom path.lz-data_layer-scatter-highlighted, svg.lz-locuszoom path.lz-data_layer-category_scatter-highlighted { stroke: rgb(24, 24, 24); stroke-opacity: 0.4; stroke-width: 4px; } - svg.lz-locuszoom path.lz-data_layer-scatter-selected { + svg.lz-locuszoom path.lz-data_layer-scatter-selected, svg.lz-locuszoom path.lz-data_layer-category_scatter-selected { stroke: rgb(24, 24, 24); stroke-opacity: 1; stroke-width: 4px; } - svg.lz-locuszoom path.lz-data_layer-scatter-faded { + svg.lz-locuszoom path.lz-data_layer-scatter-faded, svg.lz-locuszoom path.lz-data_layer-category_scatter-faded { fill-opacity: 0.1; stroke-opacity: 0.1; } - svg.lz-locuszoom path.lz-data_layer-scatter-hidden { + svg.lz-locuszoom path.lz-data_layer-scatter-hidden, svg.lz-locuszoom path.lz-data_layer-category_scatter-hidden { display: none; } - svg.lz-locuszoom text.lz-data_layer-scatter-label { + svg.lz-locuszoom text.lz-data_layer-scatter-label, svg.lz-locuszoom text.lz-data_layer-category_scatter-label { fill: rgb(24, 24, 24); fill-opacity: 1; alignment-baseline: middle; } - svg.lz-locuszoom line.lz-data_layer-scatter-label { + svg.lz-locuszoom line.lz-data_layer-scatter-label, svg.lz-locuszoom line.lz-data_layer-category_scatter-label { stroke: rgb(24, 24, 24); stroke-opacity: 1; stroke-width: 1px; } diff --git a/examples/credible_sets.html b/examples/credible_sets.html index acca0487..d54190a3 100644 --- a/examples/credible_sets.html +++ b/examples/credible_sets.html @@ -8,7 +8,7 @@ - + diff --git a/examples/phewas_scatter.html b/examples/phewas_scatter.html index 7d2627b6..4220d165 100644 --- a/examples/phewas_scatter.html +++ b/examples/phewas_scatter.html @@ -232,8 +232,8 @@

API (for developers)

url: apiBase + "phewas_" + String(variantChrom) + "_" + String(variantPosition) + "_C-T.json", params: { build: ["GRCh37"] } }]) - .add("gene", ["GeneLZ", { url: apiBase + "genes-for-phewas.json", params: {source: 2} }]) - .add("constraint", ["GeneConstraintLZ", { url: apiBase + "genes_" + String(variantChrom - 250000) + "-" + String(variantChrom + 250000) + ".json" }]); + .add("gene", ["GeneLZ", { url: apiBase + "genes_10_114508349-115008349.json", params: {source: 2} }]) + .add("constraint", ["GeneConstraintLZ", { url: apiBase + "gene_constraints_10_114508349-115008349.json" }]); } // Static blobs (independent of host) dataSources diff --git a/gulpfile.js b/gulpfile.js index 4b3030ab..70366d65 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,4 +1,5 @@ /* global require */ +var fs = require("fs"); var gulp = require("gulp"); var concat = require("gulp-concat"); var eslint = require("gulp-eslint"); @@ -7,7 +8,7 @@ var sass = require("gulp-sass"); var sourcemaps = require("gulp-sourcemaps"); var uglify = require("gulp-uglify"); var gutil = require("gulp-util"); -var wrap = require("gulp-wrap"); +var wrapJS = require("gulp-wrap-js"); var argv = require("yargs").argv; var files = require("./files.js"); @@ -51,9 +52,10 @@ gulp.task("test", ["lint"], function () { // Concatenate all app-specific JS libraries into unminified and minified single app files gulp.task("app_js", ["test"], function() { + var moduleTemplate = fs.readFileSync("./assets/js/app/wrapper.txt", "utf8"); gulp.src(files.app_build) .pipe(concat("dist/locuszoom.app.js")) - .pipe(wrap({ src: "./assets/js/app/wrapper.txt"})) + .pipe(wrapJS(moduleTemplate)) .pipe(gulp.dest(".")) .on("end", function() { gutil.log(gutil.colors.bold.white.bgBlue(" Generated locuszoom.app.js ")); @@ -62,13 +64,11 @@ gulp.task("app_js", ["test"], function() { gutil.log(gutil.colors.bold.white.bgRed(" FAILED to generate locuszoom.app.js ")); }); gulp.src(files.app_build) - // FIXME: Source maps on the app bundle are temporarily disabled because gulp-wrap is not sourcemap compatible; - // hence line numbers will be slightly off. Will revisit in future. - // .pipe(sourcemaps.init()) + .pipe(sourcemaps.init()) .pipe(concat("dist/locuszoom.app.min.js")) - .pipe(wrap({ src: "./assets/js/app/wrapper.txt"})) + .pipe(wrapJS(moduleTemplate)) .pipe(uglify()) - // .pipe(sourcemaps.write(".")) + .pipe(sourcemaps.write(".")) .pipe(gulp.dest(".")) .on("end", function() { gutil.log(gutil.colors.bold.white.bgBlue(" Generated locuszoom.app.min.js ")); diff --git a/index.html b/index.html index 86f4f189..42bc1d9c 100644 --- a/index.html +++ b/index.html @@ -61,7 +61,7 @@

Top Hits

Get LocusZoom.js

-
Current stable release: v0.7.1
+
Current stable release: v0.7.2

To use LocusZoom.js in your application you will need both LocusZoom's compiled Javascript and CSS and two third-party Javascript libraries (available here in a single vendor bundle).

@@ -82,7 +82,7 @@
Javascript
CSS
@@ -95,7 +95,7 @@
CSS
Dependencies
diff --git a/package-lock.json b/package-lock.json index 786fe92b..ddf3651c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "locuszoom", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -429,41 +429,6 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, - "bufferstreams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.0.1.tgz", - "integrity": "sha1-z7GtlWjTujz+k1upq92VLeiKqyo=", - "dev": true, - "requires": { - "readable-stream": "1.1.14" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -728,15 +693,6 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, - "consolidate": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz", - "integrity": "sha1-WiUEe8dvcwcmZ8jLUsmJiI9JTGM=", - "dev": true, - "requires": { - "bluebird": "3.5.0" - } - }, "content-type-parser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.1.tgz", @@ -1065,12 +1021,6 @@ "es6-symbol": "3.1.1" } }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", - "dev": true - }, "es6-symbol": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", @@ -1285,6 +1235,24 @@ "object-assign": "4.1.1" } }, + "estemplate": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/estemplate/-/estemplate-0.5.1.tgz", + "integrity": "sha1-FxSp1GGQc4rJWLyv1J4CnNpWo54=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "4.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + } + } + }, "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", @@ -1577,15 +1545,6 @@ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", "dev": true }, - "fs-readfile-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz", - "integrity": "sha1-gAI4I5gfn//+AWCei+Zo9prknnA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3150,22 +3109,44 @@ } } }, - "gulp-wrap": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.13.0.tgz", - "integrity": "sha1-kPsLSieiZkM4Mv98YSLbXB7olMY=", + "gulp-wrap-js": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/gulp-wrap-js/-/gulp-wrap-js-0.4.1.tgz", + "integrity": "sha1-3uYqpISqupVHqT0f9c0MPQvtwDE=", "dev": true, "requires": { - "consolidate": "0.14.5", - "es6-promise": "3.3.1", - "fs-readfile-promise": "2.0.1", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "estemplate": "0.5.1", "gulp-util": "3.0.8", - "js-yaml": "3.9.1", - "lodash": "4.17.4", - "node.extend": "1.1.6", "through2": "2.0.3", - "tryit": "1.0.3", - "vinyl-bufferstream": "1.0.1" + "vinyl-sourcemaps-apply": "0.1.4" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.1.4.tgz", + "integrity": "sha1-xfy9Q+LyOEI8LcmL3db3m3K8NFs=", + "dev": true, + "requires": { + "source-map": "0.1.43" + } + } } }, "gulplog": { @@ -3428,12 +3409,6 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, - "is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", - "dev": true - }, "is-absolute": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", @@ -4489,15 +4464,6 @@ } } }, - "node.extend": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", - "integrity": "sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y=", - "dev": true, - "requires": { - "is": "3.2.1" - } - }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -5825,12 +5791,6 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, - "tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", - "dev": true - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6029,15 +5989,6 @@ "replace-ext": "0.0.1" } }, - "vinyl-bufferstream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz", - "integrity": "sha1-BTeGn1gO/6TKRay0dXnkuf5jCBo=", - "dev": true, - "requires": { - "bufferstreams": "1.0.1" - } - }, "vinyl-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", diff --git a/package.json b/package.json index b2b0fc73..72d37048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "locuszoom", - "version": "0.7.1", + "version": "0.7.2", "description": "Generate interactive visualizations of statistical genetic data", "keywords": [ "visualization", @@ -49,7 +49,7 @@ "gulp-uglify": "~2.0.1", "gulp-util": "~3.0.8", "gulp-watch": "~4.3.11", - "gulp-wrap": "~0.13.0", + "gulp-wrap-js": "^0.4.1", "jsdoc": "^3.5.5", "jsdom": "9.12.0", "mocha": "~4.0.1", diff --git a/staticdata/gene_constraints_10_114508349-115008349.json b/staticdata/gene_constraints_10_114508349-115008349.json new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/DataLayer.js b/test/unit/DataLayer.js index f5431e4e..8524d671 100644 --- a/test/unit/DataLayer.js +++ b/test/unit/DataLayer.js @@ -217,18 +217,25 @@ describe("LocusZoom.DataLayer", function(){ x_axis: { field: "x" } }; this.datalayer = new LocusZoom.DataLayer(this.layout); + + this.datalayer.data = []; + assert.deepEqual(this.datalayer.getAxisExtent("x"), [], "No extent is returned if basic criteria cannot be met"); + this.datalayer.data = [ { x: 1 }, { x: 2 }, { x: 3 }, { x: 4 } ]; assert.deepEqual(this.datalayer.getAxisExtent("x"), [1, 4]); + this.datalayer.data = [ { x: 200 }, { x: -73 }, { x: 0 }, { x: 38 } ]; assert.deepEqual(this.datalayer.getAxisExtent("x"), [-73, 200]); + this.datalayer.data = [ { x: 6 } ]; assert.deepEqual(this.datalayer.getAxisExtent("x"), [6, 6]); + this.datalayer.data = [ { x: "apple" }, { x: "pear" }, { x: "orange" } ]; @@ -285,7 +292,11 @@ describe("LocusZoom.DataLayer", function(){ this.datalayer.data = [ { x: 1 }, { x: 2 }, { x: 3 }, { x: 4 } ]; - assert.deepEqual(this.datalayer.getAxisExtent("x"), [0, 4]); + assert.deepEqual(this.datalayer.getAxisExtent("x"), [0, 4], "Increase extent exactly to the boundaries when no padding is specified"); + + this.datalayer.data = []; + assert.deepEqual(this.datalayer.getAxisExtent("x"), [0, 3], "If there is no data, use the specified min_extent as given"); + this.layout = { id: "test", x_axis: { @@ -299,15 +310,18 @@ describe("LocusZoom.DataLayer", function(){ this.datalayer.data = [ { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 } ]; - assert.deepEqual(this.datalayer.getAxisExtent("x"), [0, 10]); + assert.deepEqual(this.datalayer.getAxisExtent("x"), [0, 10], "Extent is enforced but no padding applied when data is far from boundary"); + this.datalayer.data = [ { x: 0.6 }, { x: 4 }, { x: 5 }, { x: 9 } ]; - assert.deepEqual(this.datalayer.getAxisExtent("x"), [-1.08, 10]); + assert.deepEqual(this.datalayer.getAxisExtent("x"), [-1.08, 10], "Extent is is enforced and padding is applied when data is close to the lower boundary"); + this.datalayer.data = [ { x: 0.4 }, { x: 4 }, { x: 5 }, { x: 9.8 } ]; - assert.deepEqual(this.datalayer.getAxisExtent("x"), [-1.48, 10.74]); + assert.deepEqual(this.datalayer.getAxisExtent("x"), [-1.48, 10.74], "Padding is enforced on both sides when data is close to both boundaries"); + }); it("applies hard floor and ceiling as defined in the layout", function() { this.layout = {