diff --git a/assets/css/locuszoom.scss b/assets/css/locuszoom.scss index ba5e8355..8acbc9ab 100644 --- a/assets/css/locuszoom.scss +++ b/assets/css/locuszoom.scss @@ -16,24 +16,6 @@ svg.#{$namespace}-locuszoom { fill-opacity: 0; } - .#{$namespace}-curtain { - rect { - fill: rgb(210,210,210); - fill-opacity: 0.85; - } - text, tspan { - fill: rgb(0,0,0); - font-weight: 600; - font-size: 14px; - text-anchor: start; - alignment-baseline: hanging; - } - tspan.dismiss { - fill: rgb(49,112,143); - cursor: pointer; - } - } - .#{$namespace}-mouse_guide { rect { fill: rgb(210,210,210); @@ -323,6 +305,114 @@ div.#{$namespace}-data_layer-tooltip-arrow_bottom_right { overflow: hidden; } +.#{$namespace}-curtain { + + position: absolute; + font-size: 2em; + font-weight: 600; + background: rgba(216,216,216,0.8); + + .#{$namespace}-curtain-content { + position: absolute; + display: block; + width: 100%; + top: 50%; + transform: translateY(-50%); + margin: 0 auto; + padding: 0px 20px; + overflow-y: auto; + } + + .#{$namespace}-curtain-dismiss { + position: absolute; + top: 0px; + right: 0px; + padding: 0.15em 0.5em; + font-size: 0.3em; + font-weight: 300; + background-color: #D8D8D8; + color: #333333; + border: 1px solid #333333; + border-radius: 0px 0px 0px 4px; + pointer-events: auto; + cursor: pointer; + } + + .#{$namespace}-curtain-dismiss:hover { + background-color: #333333; + color: #D8D8D8; + } + +} + +.#{$namespace}-loader { + + position: absolute; + font-family: "Helvetica Neue", Helvetica, Aria, sans-serif; + font-size: 12px; + padding: 6px; + background: rgba(240,235,228,1); + border: 1px solid #{$default_black}; + border-radius: 4px; + box-shadow: 2px 2px 2px #{$default_black_shadow}; + + .#{$namespace}-loader-content { + position: relative; + display: block; + width: 100%; + } + + .#{$namespace}-loader-cancel { + position: absolute; + top: -1px; + right: -1px; + padding: 2px 4px; + font-size: 9px; + font-weight: 300; + background-color: #D8D8D8; + color: #333333; + border: 1px solid #333333; + border-radius: 0px 4px 0px 4px; + pointer-events: auto; + cursor: pointer; + } + + .#{$namespace}-loader-cancel:hover { + background-color: #333333; + color: #D8D8D8; + } + + .#{$namespace}-loader-progress-container { + position: relative; + display: block; + width: 100%; + height: 2px; + padding-top: 6px; + } + + .#{$namespace}-loader-progress { + position: absolute; + left: 0%; + width: 0%; + height: 2px; + background-color: #{$default_black_shadow}; + } + + .#{$namespace}-loader-progress-animated { + animation-name: #{$namespace}-loader-animate; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + } + +} + +@keyframes #{$namespace}-loader-animate { + 0% { width: 0%; left: 0%; } + 50% { width: 100%; left: 0%; } + 100% { width: 0%; left: 100%; } +} + .#{$namespace}-locuszoom-controls { font-family: "Helvetica Neue", Helvetica, Aria, sans-serif; font-size: 80%; @@ -420,29 +510,27 @@ div.#{$namespace}-panel-description { div.#{$namespace}-panel-boundary { position: absolute; - height: 4px; - width: 200px; - border-radius: 2px; - background: rgba(235,240,228,0.5); - border: 1px solid #{$default_black_shadow}; + height: 3px; + cursor: row-resize; + display: block; + padding-top: 10px; + padding-bottom: 10px; } -div.#{$namespace}-panel-boundary { - position: absolute; - height: 3px; - width: 200px; +div.#{$namespace}-panel-boundary span { + display: block; border-radius: 1px; - background: rgba(216,216,216,0.4); - border: 1px solid #{$default_black_shadow}; - cursor: row-resize; + background: rgba(216,216,216,0); + border: 1px solid rgba(216,216,216,0); + height: 3px; } -div.#{$namespace}-panel-boundary:hover { +div.#{$namespace}-panel-boundary:hover span { background: rgba(216,216,216,1); border: 1px solid #{$default_black}; } -div.#{$namespace}-panel-boundary:active { +div.#{$namespace}-panel-boundary:active span { background: rgba(51,51,51,1); border: 1px solid rgba(216,216,216,1); } diff --git a/assets/js/app/Instance.js b/assets/js/app/Instance.js index 06a10fec..b1df8505 100644 --- a/assets/js/app/Instance.js +++ b/assets/js/app/Instance.js @@ -52,6 +52,7 @@ LocusZoom.Instance = function(id, datasource, layout) { // Event hooks this.event_hooks = { "layout_changed": [], + "data_requested": [], "data_rendered": [], "element_clicked": [] }; @@ -91,7 +92,9 @@ LocusZoom.Instance = function(id, datasource, layout) { } return { x: x_offset + bounding_client_rect.left, - y: y_offset + bounding_client_rect.top + y: y_offset + bounding_client_rect.top, + width: bounding_client_rect.width, + height: bounding_client_rect.height }; }; @@ -182,7 +185,7 @@ LocusZoom.Instance.prototype.initializeLayout = function(){ * Calculate appropriate instance dimesions from panels contained within and update instance */ LocusZoom.Instance.prototype.setDimensions = function(width, height){ - + var id; // Update minimum allowable width and height by aggregating minimums from panels. @@ -225,10 +228,6 @@ LocusZoom.Instance.prototype.setDimensions = function(width, height){ this.panels[panel_id].controls.position(); } }.bind(this)); - // Reposition panel boundaries if showing - if (this.panel_boundaries && this.panel_boundaries.showing){ - this.panel_boundaries.position(); - } } // If width and height arguments were NOT passed (and panels exist) then determine the instance dimensions @@ -260,6 +259,14 @@ LocusZoom.Instance.prototype.setDimensions = function(width, height){ // If the instance has been initialized then trigger some necessary render functions if (this.initialized){ + // Reposition panel boundaries if showing + if (this.panel_boundaries && this.panel_boundaries.showing){ + this.panel_boundaries.position(); + } + // Reposition plot curtain and loader + this.curtain.update(); + this.loader.update(); + // Reposition UI layer this.ui.render(); } @@ -487,40 +494,138 @@ LocusZoom.Instance.prototype.initialize = function(){ }; this.ui.initialize(); - // Create the curtain object with svg element and drop/raise methods - var curtain_svg = this.svg.append("g") - .attr("class", "lz-curtain").style("display", "none") - .attr("id", this.id + ".curtain"); + // Create the curtain object with show/update/hide methods this.curtain = { - svg: curtain_svg, - drop: function(message){ - this.svg.style("display", null); - if (typeof message != "undefined"){ - try { - this.svg.select("text").selectAll("tspan").remove(); - message.split("\n").forEach(function(line){ - this.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "1.5em").text(line); + showing: false, + selector: null, + content_selector: null, + show: function(content, css){ + // Generate curtain + if (!this.curtain.showing){ + this.curtain.selector = d3.select(this.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.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "2.5em") - .attr("class", "dismiss").text("Dismiss") - .on("click", function(){ - this.raise(); - }.bind(this)); - } catch (e){ - console.error("LocusZoom tried to render an error message but it's not a string:", message); - } + this.curtain.showing = true; } - }, - raise: function(){ - this.svg.style("display", "none"); - } + return this.curtain.update(content, css); + }.bind(this), + update: function(content, css){ + if (!this.curtain.showing){ return this.curtain; } + // Apply CSS if provided + if (typeof css == "object" && css != null){ + this.curtain.selector.style(css); + } + // Update size and position + var plot_page_origin = this.getPageOrigin(); + this.curtain.selector.style({ + top: plot_page_origin.y + "px", + left: plot_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), + hide: function(){ + if (!this.curtain.showing){ 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) + }; + + // Create the loader object with show/update/animate/setPercentCompleted/hide methods + this.loader = { + showing: false, + selector: null, + content_selector: null, + progress_selector: null, + cancel_selector: null, + show: function(content){ + // Generate loader + if (!this.loader.showing){ + this.loader.selector = d3.select(this.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), + update: function(content, percent){ + if (!this.loader.showing){ return this.loader; } + // 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 plot_page_origin = this.getPageOrigin(); + var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); + this.loader.selector.style({ + top: (plot_page_origin.y + this.layout.height - loader_boundrect.height - padding) + "px", + left: (plot_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), + animate: function(){ + // For when it is impossible to update with percent checkpoints - animate the loader in perpetual motion + this.loader.progress_selector.classed("lz-loader-progress-animated", true); + return this.loader; + }.bind(this), + setPercentCompleted: function(percent){ + this.loader.progress_selector.classed("lz-loader-progress-animated", false); + return this.loader.update(null, percent); + }.bind(this), + hide: function(){ + if (!this.loader.showing){ 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) }; - this.curtain.svg.append("rect").attr("width", "100%").attr("height", "100%"); - this.curtain.svg.append("text") - .attr("id", this.id + ".curtain_text") - .attr("x", "1em").attr("y", "0em"); // Create the panel_boundaries object with show/position/hide methods this.panel_boundaries = { @@ -531,11 +636,12 @@ LocusZoom.Instance.prototype.initialize = function(){ selectors: [], show: function(){ // Generate panel boundaries - if (!this.showing){ + if (!this.showing && !this.parent.curtain.showing){ 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 panels"); + 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)); @@ -569,29 +675,37 @@ LocusZoom.Instance.prototype.initialize = function(){ this.showing = true; } this.position(); + return this; }, 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 - 2; + 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)); + return this; }, hide: function(){ + if (!this.showing){ return this; } // Remove panel boundaries this.selectors.forEach(function(selector){ selector.remove(); }); this.selectors = []; this.showing = false; + return this; } }; @@ -652,16 +766,19 @@ LocusZoom.Instance.prototype.initialize = function(){ } // Update all control element values this.update(); + return this; }, update: function(){ this.div.attr("width", this.parent.layout.width); var display_width = this.parent.layout.width.toString().indexOf(".") == -1 ? this.parent.layout.width : this.parent.layout.width.toFixed(2); var display_height = this.parent.layout.height.toString().indexOf(".") == -1 ? this.parent.layout.height : this.parent.layout.height.toFixed(2); this.dimensions.text(display_width + "px × " + display_height + "px"); + return this; }, hide: function(){ this.div.remove(); this.showing = false; + return this; }, generateBase64SVG: function(){ return Q.fcall(function () { @@ -804,6 +921,11 @@ LocusZoom.Instance.prototype.applyState = function(new_state){ this.state[property] = new_state[property]; } + this.emit("data_requested"); + this.panel_ids_by_y_index.forEach(function(panel_id){ + this.panels[panel_id].emit("data_requested"); + }.bind(this)); + this.remap_promises = []; for (var id in this.panels){ this.remap_promises.push(this.panels[id].reMap()); diff --git a/assets/js/app/Panel.js b/assets/js/app/Panel.js index b4e54e02..7c5d24f1 100644 --- a/assets/js/app/Panel.js +++ b/assets/js/app/Panel.js @@ -78,6 +78,7 @@ LocusZoom.Panel = function(layout, parent) { // Event hooks this.event_hooks = { "layout_changed": [], + "data_requested": [], "data_rendered": [], "element_clicked": [] }; @@ -211,7 +212,11 @@ LocusZoom.Panel.prototype.setDimensions = function(width, height){ this.svg.clipRect.attr("width", this.layout.width).attr("height", this.layout.height); } - if (this.initialized){ this.render(); } + if (this.initialized){ + this.render(); + this.curtain.update(); + this.loader.update(); + } return this; }; @@ -270,41 +275,138 @@ LocusZoom.Panel.prototype.initialize = function(){ .attr("id", this.getBaseId() + ".panel") .attr("clip-path", "url(#" + this.getBaseId() + ".clip)"); - // Append a curtain element with svg element and drop/raise methods - var panel_curtain_svg = this.svg.container.append("g") - .attr("id", this.getBaseId() + ".curtain") - .attr("clip-path", "url(#" + this.getBaseId() + ".clip)") - .attr("class", "lz-curtain").style("display", "none"); + // Create the curtain object with show/update/hide methods this.curtain = { - svg: panel_curtain_svg, - drop: function(message){ - this.svg.style("display", null); - if (typeof message != "undefined"){ - try { - this.svg.select("text").selectAll("tspan").remove(); - message.split("\n").forEach(function(line){ - this.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "1.5em").text(line); + showing: false, + selector: null, + content_selector: null, + show: function(content, css){ + // Generate curtain + if (!this.curtain.showing){ + this.curtain.selector = d3.select(this.parent.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.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "2.5em") - .attr("class", "dismiss").text("Dismiss") - .on("click", function(){ - this.raise(); - }.bind(this)); - } catch (e){ - console.error("LocusZoom tried to render an error message but it's not a string:", message); - } + this.curtain.showing = true; } - }, - raise: function(){ - this.svg.style("display", "none"); - } + return this.curtain.update(content, css); + }.bind(this), + update: function(content, css){ + if (!this.curtain.showing){ return this.curtain; } + // Apply CSS if provided + if (typeof css == "object"){ + this.curtain.selector.style(css); + } + // Update size and position + var panel_page_origin = this.getPageOrigin(); + this.curtain.selector.style({ + top: panel_page_origin.y + "px", + left: panel_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), + hide: function(){ + if (!this.curtain.showing){ 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) + }; + + // Create the loader object with show/update/animate/setPercentCompleted/hide methods + this.loader = { + showing: false, + selector: null, + content_selector: null, + progress_selector: null, + cancel_selector: null, + show: function(content){ + // Generate loader + if (!this.loader.showing){ + this.loader.selector = d3.select(this.parent.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), + update: function(content, percent){ + if (!this.loader.showing){ return this.loader; } + // 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 panel_page_origin = this.getPageOrigin(); + var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); + this.loader.selector.style({ + top: (panel_page_origin.y + this.layout.height - loader_boundrect.height - padding) + "px", + left: (panel_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), + animate: function(){ + // For when it is impossible to update with percent checkpoints - animate the loader in perpetual motion + this.loader.progress_selector.classed("lz-loader-progress-animated", true); + return this.loader; + }.bind(this), + setPercentCompleted: function(percent){ + this.loader.progress_selector.classed("lz-loader-progress-animated", false); + return this.loader.update(null, percent); + }.bind(this), + hide: function(){ + if (!this.loader.showing){ 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) }; - this.curtain.svg.append("rect").attr("width", "100%").attr("height", "100%"); - this.curtain.svg.append("text") - .attr("id", this.getBaseId() + ".curtain_text") - .attr("x", "1em").attr("y", "0em"); // Initialize controls element this.controls = { @@ -312,7 +414,8 @@ LocusZoom.Panel.prototype.initialize = function(){ hide_timeout: null, link_selectors: {}, show: function(){ - if (!this.layout.controls || this.controls.selector){ return; } + if (!this.layout.controls || this.controls.selector){ return this.controls; } + if (this.curtain.showing || this.parent.curtain.showing){ return this.controls; } this.controls.selector = d3.select(this.parent.svg.node().parentNode).insert("div", ".lz-data_layer-tooltip") .attr("class", "lz-locuszoom-controls lz-locuszoom-panel-controls") .attr("id", this.getBaseId() + ".controls") @@ -354,7 +457,7 @@ LocusZoom.Panel.prototype.initialize = function(){ .style({ "font-weight": "bold" }) .text("?") .on("click", function(){ - if (this.controls.description.is_showing){ + if (this.controls.description.showing){ this.controls.description.hide(); } else { this.controls.description.show(); @@ -362,7 +465,7 @@ LocusZoom.Panel.prototype.initialize = function(){ } }.bind(this)); this.controls.description = { - is_showing: false, + showing: false, selector: null, show: function(){ this.controls.link_selectors.description.attr("class", "lz-panel-controls-button-selected"); @@ -370,9 +473,11 @@ LocusZoom.Panel.prototype.initialize = function(){ .attr("class", "lz-panel-description") .attr("id", this.getBaseId() + ".description") .html(this.layout.description); - this.controls.description.is_showing = true; + this.controls.description.showing = true; + return this.controls.description; }.bind(this), position: function(){ + if (!this.controls.description.showing){ return this.controls.description; } var padding = 4; // is there a better place to store this? var page_origin = this.getPageOrigin(); var controls_client_rect = this.controls.selector.node().getBoundingClientRect(); @@ -380,13 +485,17 @@ LocusZoom.Panel.prototype.initialize = function(){ var top = (page_origin.y + controls_client_rect.height + padding).toString() + "px"; var left = Math.max(page_origin.x + this.layout.width - desc_client_rect.width - padding, page_origin.x + padding).toString() + "px"; this.controls.description.selector.style({ top: top, left: left }); + return this.controls.description; }.bind(this), hide: function(){ + if (!this.controls.description.showing){ return this.controls.description; } this.controls.link_selectors.description.attr("class", "lz-panel-controls-button"); this.controls.description.selector.remove(); - this.controls.description.is_showing = false; + this.controls.description.showing = false; + return this.controls.description; }.bind(this) }; + return this.controls; } // Remove button if (this.layout.controls.remove){ @@ -397,7 +506,7 @@ LocusZoom.Panel.prototype.initialize = function(){ .text("×") .on("click", function(){ // Hide description and controls - if (this.controls.description && this.controls.description.is_showing){ this.controls.description.hide(); } + if (this.controls.description && this.controls.description.showing){ this.controls.description.hide(); } this.controls.hide(); // Remove mouse event listeners for these controls d3.select(this.parent.svg.node().parentNode).on("mouseover." + this.getBaseId() + ".controls", null); @@ -405,16 +514,18 @@ LocusZoom.Panel.prototype.initialize = function(){ // Remove the panel this.parent.removePanel(this.id); }.bind(this)); + return this.controls; } }.bind(this), position: function(){ + if (!this.layout.controls || !this.controls.selector){ return this.controls; } var page_origin = this.getPageOrigin(); var client_rect = this.controls.selector.node().getBoundingClientRect(); var top = page_origin.y.toString() + "px"; var left = (page_origin.x + this.layout.width - client_rect.width).toString() + "px"; this.controls.selector.style({ position: "absolute", top: top, left: left }); // Position description box if it's showing - if (this.controls.description && this.controls.description.is_showing){ + if (this.controls.description && this.controls.description.showing){ this.controls.description.position(); } // Apply appropriate classes to reposition buttons as needed @@ -424,15 +535,17 @@ LocusZoom.Panel.prototype.initialize = function(){ if (this.controls.link_selectors.reposition_down){ this.controls.link_selectors.reposition_down.attr("class", (this.layout.y_index == this.parent.panel_ids_by_y_index.length - 1) ? "lz-panel-controls-button-disabled" : "lz-panel-controls-button"); } + return this.controls; }.bind(this), hide: function(){ - if (!this.layout.controls || !this.controls.selector){ return; } + if (!this.layout.controls || !this.controls.selector){ return this.controls; } // Do not hide if this panel is showing a description - if (this.controls.description && this.controls.description.is_showing){ return; } + if (this.controls.description && this.controls.description.showing){ return this.controls; } // Do not hide if actively in an instance-level drag event - if (this.parent.ui.dragging || this.parent.panel_boundaries.dragging){ return; } + if (this.parent.ui.dragging || this.parent.panel_boundaries.dragging){ return this.controls; } this.controls.selector.remove(); this.controls.selector = null; + return this.controls; }.bind(this) }; @@ -574,7 +687,7 @@ LocusZoom.Panel.prototype.reMap = function(){ this.data_promises.push(this.data_layers[id].reMap()); } catch (error) { console.log(error); - this.curtain.drop(error); + this.curtain.show(error); } } // When all finished trigger a render @@ -588,7 +701,7 @@ LocusZoom.Panel.prototype.reMap = function(){ }.bind(this)) .catch(function(error){ console.log(error); - this.curtain.drop(error); + this.curtain.show(error); }.bind(this)); }; diff --git a/demo.html b/demo.html index f6935f90..a337a412 100644 --- a/demo.html +++ b/demo.html @@ -69,7 +69,18 @@

Top Hits

layout = LocusZoom.mergeLayouts(layout, LocusZoom.StandardLayout); // Populate the div with a LocusZoom plot using the default layout - var demo_instance = LocusZoom.populate("#lz-1", data_sources, layout); + var plot = LocusZoom.populate("#lz-1", data_sources, layout); + + // Create event hooks to clear the loader whenever a panel renders new data + plot.layout.panels.forEach(function(panel){ + plot.panels[panel.id].loader.show("Loading...").animate(); + plot.panels[panel.id].on("data_requested", function(){ + this.loader.show("Loading...").animate(); + }); + plot.panels[panel.id].on("data_rendered", function(){ + this.loader.hide(); + }); + }); /********************************************************************************** All of the following code sets up an example form with top hit buttons to jump to @@ -119,7 +130,7 @@

Top Hits

start = +pos - 300000 end = +pos + 300000 } - demo_instance.applyState({ chr: chr, start: start, end: end}); + plot.applyState({ chr: chr, start: start, end: end }); } function jumpTo(region) { @@ -132,16 +143,16 @@

Top Hits

start = +pos - 300000 end = +pos + 300000 } - demo_instance.applyState({ chr: chr, start: start, end: end, ldrefvar: "" }); + plot.applyState({ chr: chr, start: start, end: end, ldrefvar: "" }); populateForms(); return(false); } // Fill demo forms with values already loaded into LocusZoom objects function populateForms(){ - $("#lz-1_region")[0].value = demo_instance.state.chr + ":" - + demo_instance.state.start + "-" - + demo_instance.state.end; + $("#lz-1_region")[0].value = plot.state.chr + ":" + + plot.state.start + "-" + + plot.state.end; } function listHits() { diff --git a/locuszoom.app.js b/locuszoom.app.js index 4d1bec9d..2e73561b 100644 --- a/locuszoom.app.js +++ b/locuszoom.app.js @@ -3168,6 +3168,7 @@ LocusZoom.Instance = function(id, datasource, layout) { // Event hooks this.event_hooks = { "layout_changed": [], + "data_requested": [], "data_rendered": [], "element_clicked": [] }; @@ -3207,7 +3208,9 @@ LocusZoom.Instance = function(id, datasource, layout) { } return { x: x_offset + bounding_client_rect.left, - y: y_offset + bounding_client_rect.top + y: y_offset + bounding_client_rect.top, + width: bounding_client_rect.width, + height: bounding_client_rect.height }; }; @@ -3298,7 +3301,7 @@ LocusZoom.Instance.prototype.initializeLayout = function(){ * Calculate appropriate instance dimesions from panels contained within and update instance */ LocusZoom.Instance.prototype.setDimensions = function(width, height){ - + var id; // Update minimum allowable width and height by aggregating minimums from panels. @@ -3341,10 +3344,6 @@ LocusZoom.Instance.prototype.setDimensions = function(width, height){ this.panels[panel_id].controls.position(); } }.bind(this)); - // Reposition panel boundaries if showing - if (this.panel_boundaries && this.panel_boundaries.showing){ - this.panel_boundaries.position(); - } } // If width and height arguments were NOT passed (and panels exist) then determine the instance dimensions @@ -3376,6 +3375,14 @@ LocusZoom.Instance.prototype.setDimensions = function(width, height){ // If the instance has been initialized then trigger some necessary render functions if (this.initialized){ + // Reposition panel boundaries if showing + if (this.panel_boundaries && this.panel_boundaries.showing){ + this.panel_boundaries.position(); + } + // Reposition plot curtain and loader + this.curtain.update(); + this.loader.update(); + // Reposition UI layer this.ui.render(); } @@ -3603,40 +3610,138 @@ LocusZoom.Instance.prototype.initialize = function(){ }; this.ui.initialize(); - // Create the curtain object with svg element and drop/raise methods - var curtain_svg = this.svg.append("g") - .attr("class", "lz-curtain").style("display", "none") - .attr("id", this.id + ".curtain"); + // Create the curtain object with show/update/hide methods this.curtain = { - svg: curtain_svg, - drop: function(message){ - this.svg.style("display", null); - if (typeof message != "undefined"){ - try { - this.svg.select("text").selectAll("tspan").remove(); - message.split("\n").forEach(function(line){ - this.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "1.5em").text(line); + showing: false, + selector: null, + content_selector: null, + show: function(content, css){ + // Generate curtain + if (!this.curtain.showing){ + this.curtain.selector = d3.select(this.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.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "2.5em") - .attr("class", "dismiss").text("Dismiss") - .on("click", function(){ - this.raise(); - }.bind(this)); - } catch (e){ - console.error("LocusZoom tried to render an error message but it's not a string:", message); - } + this.curtain.showing = true; } - }, - raise: function(){ - this.svg.style("display", "none"); - } + return this.curtain.update(content, css); + }.bind(this), + update: function(content, css){ + if (!this.curtain.showing){ return this.curtain; } + // Apply CSS if provided + if (typeof css == "object" && css != null){ + this.curtain.selector.style(css); + } + // Update size and position + var plot_page_origin = this.getPageOrigin(); + this.curtain.selector.style({ + top: plot_page_origin.y + "px", + left: plot_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), + hide: function(){ + if (!this.curtain.showing){ 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) + }; + + // Create the loader object with show/update/animate/setPercentCompleted/hide methods + this.loader = { + showing: false, + selector: null, + content_selector: null, + progress_selector: null, + cancel_selector: null, + show: function(content){ + // Generate loader + if (!this.loader.showing){ + this.loader.selector = d3.select(this.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), + update: function(content, percent){ + if (!this.loader.showing){ return this.loader; } + // 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 plot_page_origin = this.getPageOrigin(); + var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); + this.loader.selector.style({ + top: (plot_page_origin.y + this.layout.height - loader_boundrect.height - padding) + "px", + left: (plot_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), + animate: function(){ + // For when it is impossible to update with percent checkpoints - animate the loader in perpetual motion + this.loader.progress_selector.classed("lz-loader-progress-animated", true); + return this.loader; + }.bind(this), + setPercentCompleted: function(percent){ + this.loader.progress_selector.classed("lz-loader-progress-animated", false); + return this.loader.update(null, percent); + }.bind(this), + hide: function(){ + if (!this.loader.showing){ 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) }; - this.curtain.svg.append("rect").attr("width", "100%").attr("height", "100%"); - this.curtain.svg.append("text") - .attr("id", this.id + ".curtain_text") - .attr("x", "1em").attr("y", "0em"); // Create the panel_boundaries object with show/position/hide methods this.panel_boundaries = { @@ -3647,11 +3752,12 @@ LocusZoom.Instance.prototype.initialize = function(){ selectors: [], show: function(){ // Generate panel boundaries - if (!this.showing){ + if (!this.showing && !this.parent.curtain.showing){ 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 panels"); + 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)); @@ -3685,29 +3791,37 @@ LocusZoom.Instance.prototype.initialize = function(){ this.showing = true; } this.position(); + return this; }, 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 - 2; + 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)); + return this; }, hide: function(){ + if (!this.showing){ return this; } // Remove panel boundaries this.selectors.forEach(function(selector){ selector.remove(); }); this.selectors = []; this.showing = false; + return this; } }; @@ -3768,16 +3882,19 @@ LocusZoom.Instance.prototype.initialize = function(){ } // Update all control element values this.update(); + return this; }, update: function(){ this.div.attr("width", this.parent.layout.width); var display_width = this.parent.layout.width.toString().indexOf(".") == -1 ? this.parent.layout.width : this.parent.layout.width.toFixed(2); var display_height = this.parent.layout.height.toString().indexOf(".") == -1 ? this.parent.layout.height : this.parent.layout.height.toFixed(2); this.dimensions.text(display_width + "px × " + display_height + "px"); + return this; }, hide: function(){ this.div.remove(); this.showing = false; + return this; }, generateBase64SVG: function(){ return Q.fcall(function () { @@ -3920,6 +4037,11 @@ LocusZoom.Instance.prototype.applyState = function(new_state){ this.state[property] = new_state[property]; } + this.emit("data_requested"); + this.panel_ids_by_y_index.forEach(function(panel_id){ + this.panels[panel_id].emit("data_requested"); + }.bind(this)); + this.remap_promises = []; for (var id in this.panels){ this.remap_promises.push(this.panels[id].reMap()); @@ -4043,6 +4165,7 @@ LocusZoom.Panel = function(layout, parent) { // Event hooks this.event_hooks = { "layout_changed": [], + "data_requested": [], "data_rendered": [], "element_clicked": [] }; @@ -4176,7 +4299,11 @@ LocusZoom.Panel.prototype.setDimensions = function(width, height){ this.svg.clipRect.attr("width", this.layout.width).attr("height", this.layout.height); } - if (this.initialized){ this.render(); } + if (this.initialized){ + this.render(); + this.curtain.update(); + this.loader.update(); + } return this; }; @@ -4235,41 +4362,138 @@ LocusZoom.Panel.prototype.initialize = function(){ .attr("id", this.getBaseId() + ".panel") .attr("clip-path", "url(#" + this.getBaseId() + ".clip)"); - // Append a curtain element with svg element and drop/raise methods - var panel_curtain_svg = this.svg.container.append("g") - .attr("id", this.getBaseId() + ".curtain") - .attr("clip-path", "url(#" + this.getBaseId() + ".clip)") - .attr("class", "lz-curtain").style("display", "none"); + // Create the curtain object with show/update/hide methods this.curtain = { - svg: panel_curtain_svg, - drop: function(message){ - this.svg.style("display", null); - if (typeof message != "undefined"){ - try { - this.svg.select("text").selectAll("tspan").remove(); - message.split("\n").forEach(function(line){ - this.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "1.5em").text(line); + showing: false, + selector: null, + content_selector: null, + show: function(content, css){ + // Generate curtain + if (!this.curtain.showing){ + this.curtain.selector = d3.select(this.parent.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.svg.select("text").append("tspan") - .attr("x", "1em").attr("dy", "2.5em") - .attr("class", "dismiss").text("Dismiss") - .on("click", function(){ - this.raise(); - }.bind(this)); - } catch (e){ - console.error("LocusZoom tried to render an error message but it's not a string:", message); - } + this.curtain.showing = true; } - }, - raise: function(){ - this.svg.style("display", "none"); - } + return this.curtain.update(content, css); + }.bind(this), + update: function(content, css){ + if (!this.curtain.showing){ return this.curtain; } + // Apply CSS if provided + if (typeof css == "object"){ + this.curtain.selector.style(css); + } + // Update size and position + var panel_page_origin = this.getPageOrigin(); + this.curtain.selector.style({ + top: panel_page_origin.y + "px", + left: panel_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), + hide: function(){ + if (!this.curtain.showing){ 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) + }; + + // Create the loader object with show/update/animate/setPercentCompleted/hide methods + this.loader = { + showing: false, + selector: null, + content_selector: null, + progress_selector: null, + cancel_selector: null, + show: function(content){ + // Generate loader + if (!this.loader.showing){ + this.loader.selector = d3.select(this.parent.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), + update: function(content, percent){ + if (!this.loader.showing){ return this.loader; } + // 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 panel_page_origin = this.getPageOrigin(); + var loader_boundrect = this.loader.selector.node().getBoundingClientRect(); + this.loader.selector.style({ + top: (panel_page_origin.y + this.layout.height - loader_boundrect.height - padding) + "px", + left: (panel_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), + animate: function(){ + // For when it is impossible to update with percent checkpoints - animate the loader in perpetual motion + this.loader.progress_selector.classed("lz-loader-progress-animated", true); + return this.loader; + }.bind(this), + setPercentCompleted: function(percent){ + this.loader.progress_selector.classed("lz-loader-progress-animated", false); + return this.loader.update(null, percent); + }.bind(this), + hide: function(){ + if (!this.loader.showing){ 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) }; - this.curtain.svg.append("rect").attr("width", "100%").attr("height", "100%"); - this.curtain.svg.append("text") - .attr("id", this.getBaseId() + ".curtain_text") - .attr("x", "1em").attr("y", "0em"); // Initialize controls element this.controls = { @@ -4277,7 +4501,8 @@ LocusZoom.Panel.prototype.initialize = function(){ hide_timeout: null, link_selectors: {}, show: function(){ - if (!this.layout.controls || this.controls.selector){ return; } + if (!this.layout.controls || this.controls.selector){ return this.controls; } + if (this.curtain.showing || this.parent.curtain.showing){ return this.controls; } this.controls.selector = d3.select(this.parent.svg.node().parentNode).insert("div", ".lz-data_layer-tooltip") .attr("class", "lz-locuszoom-controls lz-locuszoom-panel-controls") .attr("id", this.getBaseId() + ".controls") @@ -4319,7 +4544,7 @@ LocusZoom.Panel.prototype.initialize = function(){ .style({ "font-weight": "bold" }) .text("?") .on("click", function(){ - if (this.controls.description.is_showing){ + if (this.controls.description.showing){ this.controls.description.hide(); } else { this.controls.description.show(); @@ -4327,7 +4552,7 @@ LocusZoom.Panel.prototype.initialize = function(){ } }.bind(this)); this.controls.description = { - is_showing: false, + showing: false, selector: null, show: function(){ this.controls.link_selectors.description.attr("class", "lz-panel-controls-button-selected"); @@ -4335,9 +4560,11 @@ LocusZoom.Panel.prototype.initialize = function(){ .attr("class", "lz-panel-description") .attr("id", this.getBaseId() + ".description") .html(this.layout.description); - this.controls.description.is_showing = true; + this.controls.description.showing = true; + return this.controls.description; }.bind(this), position: function(){ + if (!this.controls.description.showing){ return this.controls.description; } var padding = 4; // is there a better place to store this? var page_origin = this.getPageOrigin(); var controls_client_rect = this.controls.selector.node().getBoundingClientRect(); @@ -4345,13 +4572,17 @@ LocusZoom.Panel.prototype.initialize = function(){ var top = (page_origin.y + controls_client_rect.height + padding).toString() + "px"; var left = Math.max(page_origin.x + this.layout.width - desc_client_rect.width - padding, page_origin.x + padding).toString() + "px"; this.controls.description.selector.style({ top: top, left: left }); + return this.controls.description; }.bind(this), hide: function(){ + if (!this.controls.description.showing){ return this.controls.description; } this.controls.link_selectors.description.attr("class", "lz-panel-controls-button"); this.controls.description.selector.remove(); - this.controls.description.is_showing = false; + this.controls.description.showing = false; + return this.controls.description; }.bind(this) }; + return this.controls; } // Remove button if (this.layout.controls.remove){ @@ -4362,7 +4593,7 @@ LocusZoom.Panel.prototype.initialize = function(){ .text("×") .on("click", function(){ // Hide description and controls - if (this.controls.description && this.controls.description.is_showing){ this.controls.description.hide(); } + if (this.controls.description && this.controls.description.showing){ this.controls.description.hide(); } this.controls.hide(); // Remove mouse event listeners for these controls d3.select(this.parent.svg.node().parentNode).on("mouseover." + this.getBaseId() + ".controls", null); @@ -4370,16 +4601,18 @@ LocusZoom.Panel.prototype.initialize = function(){ // Remove the panel this.parent.removePanel(this.id); }.bind(this)); + return this.controls; } }.bind(this), position: function(){ + if (!this.layout.controls || !this.controls.selector){ return this.controls; } var page_origin = this.getPageOrigin(); var client_rect = this.controls.selector.node().getBoundingClientRect(); var top = page_origin.y.toString() + "px"; var left = (page_origin.x + this.layout.width - client_rect.width).toString() + "px"; this.controls.selector.style({ position: "absolute", top: top, left: left }); // Position description box if it's showing - if (this.controls.description && this.controls.description.is_showing){ + if (this.controls.description && this.controls.description.showing){ this.controls.description.position(); } // Apply appropriate classes to reposition buttons as needed @@ -4389,15 +4622,17 @@ LocusZoom.Panel.prototype.initialize = function(){ if (this.controls.link_selectors.reposition_down){ this.controls.link_selectors.reposition_down.attr("class", (this.layout.y_index == this.parent.panel_ids_by_y_index.length - 1) ? "lz-panel-controls-button-disabled" : "lz-panel-controls-button"); } + return this.controls; }.bind(this), hide: function(){ - if (!this.layout.controls || !this.controls.selector){ return; } + if (!this.layout.controls || !this.controls.selector){ return this.controls; } // Do not hide if this panel is showing a description - if (this.controls.description && this.controls.description.is_showing){ return; } + if (this.controls.description && this.controls.description.showing){ return this.controls; } // Do not hide if actively in an instance-level drag event - if (this.parent.ui.dragging || this.parent.panel_boundaries.dragging){ return; } + if (this.parent.ui.dragging || this.parent.panel_boundaries.dragging){ return this.controls; } this.controls.selector.remove(); this.controls.selector = null; + return this.controls; }.bind(this) }; @@ -4539,7 +4774,7 @@ LocusZoom.Panel.prototype.reMap = function(){ this.data_promises.push(this.data_layers[id].reMap()); } catch (error) { console.log(error); - this.curtain.drop(error); + this.curtain.show(error); } } // When all finished trigger a render @@ -4553,7 +4788,7 @@ LocusZoom.Panel.prototype.reMap = function(){ }.bind(this)) .catch(function(error){ console.log(error); - this.curtain.drop(error); + this.curtain.show(error); }.bind(this)); }; diff --git a/locuszoom.css b/locuszoom.css index 065219bf..0763f4b6 100644 --- a/locuszoom.css +++ b/locuszoom.css @@ -7,18 +7,6 @@ svg.lz-locuszoom { svg.lz-locuszoom rect.lz-clickarea { fill: black; fill-opacity: 0; } - svg.lz-locuszoom .lz-curtain rect { - fill: #d2d2d2; - fill-opacity: 0.85; } - svg.lz-locuszoom .lz-curtain text, svg.lz-locuszoom .lz-curtain tspan { - fill: black; - font-weight: 600; - font-size: 14px; - text-anchor: start; - alignment-baseline: hanging; } - svg.lz-locuszoom .lz-curtain tspan.dismiss { - fill: #31708f; - cursor: pointer; } svg.lz-locuszoom .lz-mouse_guide rect { fill: #d2d2d2; fill-opacity: 0.85; } @@ -263,6 +251,95 @@ div.lz-data_layer-tooltip-arrow_bottom_right { display: inline-block; overflow: hidden; } +.lz-curtain { + position: absolute; + font-size: 2em; + font-weight: 600; + background: rgba(216, 216, 216, 0.8); } + .lz-curtain .lz-curtain-content { + position: absolute; + display: block; + width: 100%; + top: 50%; + transform: translateY(-50%); + margin: 0 auto; + padding: 0px 20px; + overflow-y: auto; } + .lz-curtain .lz-curtain-dismiss { + position: absolute; + top: 0px; + right: 0px; + padding: 0.15em 0.5em; + font-size: 0.3em; + font-weight: 300; + background-color: #D8D8D8; + color: #333333; + border: 1px solid #333333; + border-radius: 0px 0px 0px 4px; + pointer-events: auto; + cursor: pointer; } + .lz-curtain .lz-curtain-dismiss:hover { + background-color: #333333; + color: #D8D8D8; } + +.lz-loader { + position: absolute; + font-family: "Helvetica Neue", Helvetica, Aria, sans-serif; + font-size: 12px; + padding: 6px; + background: #f0ebe4; + border: 1px solid rgba(24, 24, 24, 1); + border-radius: 4px; + box-shadow: 2px 2px 2px rgba(24, 24, 24, 0.4); } + .lz-loader .lz-loader-content { + position: relative; + display: block; + width: 100%; } + .lz-loader .lz-loader-cancel { + position: absolute; + top: -1px; + right: -1px; + padding: 2px 4px; + font-size: 9px; + font-weight: 300; + background-color: #D8D8D8; + color: #333333; + border: 1px solid #333333; + border-radius: 0px 4px 0px 4px; + pointer-events: auto; + cursor: pointer; } + .lz-loader .lz-loader-cancel:hover { + background-color: #333333; + color: #D8D8D8; } + .lz-loader .lz-loader-progress-container { + position: relative; + display: block; + width: 100%; + height: 2px; + padding-top: 6px; } + .lz-loader .lz-loader-progress { + position: absolute; + left: 0%; + width: 0%; + height: 2px; + background-color: rgba(24, 24, 24, 0.4); } + .lz-loader .lz-loader-progress-animated { + animation-name: lz-loader-animate; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; } + +@keyframes lz-loader-animate { + 0% { + width: 0%; + left: 0%; } + 50% { + width: 100%; + left: 0%; } + 100% { + width: 0%; + left: 100%; } } + .lz-locuszoom-controls { font-family: "Helvetica Neue", Helvetica, Aria, sans-serif; font-size: 80%; @@ -346,27 +423,25 @@ div.lz-panel-description { border-radius: 4px; box-shadow: 2px 2px 2px rgba(24, 24, 24, 0.4); } -div.lz-panel-boundary { - position: absolute; - height: 4px; - width: 200px; - border-radius: 2px; - background: rgba(235, 240, 228, 0.5); - border: 1px solid rgba(24, 24, 24, 0.4); } - div.lz-panel-boundary { position: absolute; height: 3px; - width: 200px; + cursor: row-resize; + display: block; + padding-top: 10px; + padding-bottom: 10px; } + +div.lz-panel-boundary span { + display: block; border-radius: 1px; - background: rgba(216, 216, 216, 0.4); - border: 1px solid rgba(24, 24, 24, 0.4); - cursor: row-resize; } + background: rgba(216, 216, 216, 0); + border: 1px solid rgba(216, 216, 216, 0); + height: 3px; } -div.lz-panel-boundary:hover { +div.lz-panel-boundary:hover span { background: #d8d8d8; border: 1px solid rgba(24, 24, 24, 1); } -div.lz-panel-boundary:active { +div.lz-panel-boundary:active span { background: #333333; border: 1px solid #d8d8d8; } diff --git a/plot_builder.html b/plot_builder.html index 0dd89760..df0bdd33 100644 --- a/plot_builder.html +++ b/plot_builder.html @@ -46,7 +46,7 @@ - +
@@ -224,9 +224,9 @@

LocusZoom Plot Builder

] }; var panel = plot.addPanel(layout); - panel.curtain.drop("loading..."); + panel.curtain.show("loading...", { "text-align": "center" }); panel.on("data_rendered", function(){ - this.curtain.raise(); + this.curtain.hide(); }.bind(panel)); } @@ -267,9 +267,9 @@

LocusZoom Plot Builder

return; } plot = LocusZoom.populate("#plot", data_sources, layout); - plot.curtain.drop("applying layout..."); + plot.curtain.show("applying layout...", { "text-align": "center" }); plot.on("data_rendered", function(){ - this.curtain.raise(); + this.curtain.hide(); }.bind(plot)); applyOnLayoutChanged(); } diff --git a/test/Instance.js b/test/Instance.js index 5742d0a6..dfc6df17 100644 --- a/test/Instance.js +++ b/test/Instance.js @@ -209,10 +209,10 @@ describe('LocusZoom.Instance', function(){ d3.select("body").append("div").attr("id", "plot"); this.instance = LocusZoom.populate("#plot"); }); - it('second-to-last child should be a ui group element', function(){ + it('last child should be a ui group element', function(){ var childNodes = this.instance.svg.node().childNodes.length; - d3.select(this.instance.svg.node().childNodes[childNodes-2]).attr("id").should.be.exactly("plot.ui"); - d3.select(this.instance.svg.node().childNodes[childNodes-2]).attr("class").should.be.exactly("lz-ui"); + d3.select(this.instance.svg.node().childNodes[childNodes-1]).attr("id").should.be.exactly("plot.ui"); + d3.select(this.instance.svg.node().childNodes[childNodes-1]).attr("class").should.be.exactly("lz-ui"); }); it('should have a ui object with ui svg selectors', function(){ this.instance.ui.should.be.an.Object; @@ -237,35 +237,6 @@ describe('LocusZoom.Instance', function(){ assert.equal(this.instance.ui.svg.style("display"), "none"); }); }); - describe("Curtain Layer", function() { - beforeEach(function(){ - d3.select("body").append("div").attr("id", "plot"); - this.instance = LocusZoom.populate("#plot"); - }); - it('last child should be a curtain group element', function(){ - d3.select(this.instance.svg.node().lastChild).attr("id").should.be.exactly("plot.curtain"); - d3.select(this.instance.svg.node().lastChild).attr("class").should.be.exactly("lz-curtain"); - }); - it('should have a curtain object with stored svg selector', function(){ - this.instance.curtain.should.be.an.Object; - this.instance.curtain.svg.should.be.an.Object; - assert.equal(this.instance.curtain.svg.html(), this.instance.svg.select("#plot\\.curtain").html()); - }); - it('should be hidden by default', function(){ - assert.equal(this.instance.curtain.svg.style("display"), "none"); - }); - it('should have a method that drops the curtain', function(){ - this.instance.curtain.drop.should.be.a.Function; - this.instance.curtain.drop(); - assert.equal(this.instance.curtain.svg.style("display"), ""); - }); - it('should have a method that raises the curtain', function(){ - this.instance.curtain.raise.should.be.a.Function; - this.instance.curtain.drop(); - this.instance.curtain.raise(); - assert.equal(this.instance.curtain.svg.style("display"), "none"); - }); - }); }); describe("Dynamic Panel Positioning", function() { @@ -371,4 +342,91 @@ describe('LocusZoom.Instance', function(){ }); }); + describe("Instance Curtain and Loader", function() { + beforeEach(function(){ + var datasources = new LocusZoom.DataSources(); + this.layout = { + width: 100, + height: 100, + min_width: 100, + min_height: 100, + resizable: false, + aspect_ratio: 1, + panels: [], + controls: false + }; + d3.select("body").append("div").attr("id", "plot"); + this.plot = LocusZoom.populate("#plot", datasources, this.layout); + }); + it("should have a curtain object with show/update/hide methods, a showing boolean, and selectors", function(){ + this.plot.should.have.property("curtain").which.is.an.Object; + this.plot.curtain.should.have.property("showing").which.is.exactly(false); + this.plot.curtain.should.have.property("show").which.is.a.Function; + this.plot.curtain.should.have.property("update").which.is.a.Function; + this.plot.curtain.should.have.property("hide").which.is.a.Function; + this.plot.curtain.should.have.property("selector").which.is.exactly(null); + this.plot.curtain.should.have.property("content_selector").which.is.exactly(null); + }); + it("should show/hide/update on command and track shown status", function(){ + this.plot.curtain.showing.should.be.false(); + this.plot.curtain.should.have.property("selector").which.is.exactly(null); + this.plot.curtain.should.have.property("content_selector").which.is.exactly(null); + this.plot.curtain.show("test content"); + this.plot.curtain.showing.should.be.true(); + this.plot.curtain.selector.empty().should.be.false(); + this.plot.curtain.content_selector.empty().should.be.false(); + this.plot.curtain.content_selector.html().should.be.exactly("test content"); + this.plot.curtain.hide(); + this.plot.curtain.showing.should.be.false(); + this.plot.curtain.should.have.property("selector").which.is.exactly(null); + this.plot.curtain.should.have.property("content_selector").which.is.exactly(null); + }); + it("should have a loader object with show/update/animate/setPercentCompleted/hide methods, a showing boolean, and selectors", function(){ + this.plot.should.have.property("loader").which.is.an.Object; + this.plot.loader.should.have.property("showing").which.is.exactly(false); + this.plot.loader.should.have.property("show").which.is.a.Function; + this.plot.loader.should.have.property("update").which.is.a.Function; + this.plot.loader.should.have.property("animate").which.is.a.Function; + this.plot.loader.should.have.property("update").which.is.a.Function; + this.plot.loader.should.have.property("setPercentCompleted").which.is.a.Function; + this.plot.loader.should.have.property("selector").which.is.exactly(null); + this.plot.loader.should.have.property("content_selector").which.is.exactly(null); + this.plot.loader.should.have.property("progress_selector").which.is.exactly(null); + }); + it("should show/hide/update on command and track shown status", function(){ + this.plot.loader.showing.should.be.false(); + this.plot.loader.should.have.property("selector").which.is.exactly(null); + this.plot.loader.should.have.property("content_selector").which.is.exactly(null); + this.plot.loader.should.have.property("progress_selector").which.is.exactly(null); + this.plot.loader.show("test content"); + this.plot.loader.showing.should.be.true(); + this.plot.loader.selector.empty().should.be.false(); + this.plot.loader.content_selector.empty().should.be.false(); + this.plot.loader.content_selector.html().should.be.exactly("test content"); + this.plot.loader.progress_selector.empty().should.be.false(); + this.plot.loader.hide(); + this.plot.loader.showing.should.be.false(); + this.plot.loader.should.have.property("selector").which.is.exactly(null); + this.plot.loader.should.have.property("content_selector").which.is.exactly(null); + this.plot.loader.should.have.property("progress_selector").which.is.exactly(null); + }); + it("should allow for animating or showing discrete percentages of completion", function(){ + this.plot.loader.show("test content").animate(); + this.plot.loader.progress_selector.classed("lz-loader-progress-animated").should.be.true(); + this.plot.loader.setPercentCompleted(15); + this.plot.loader.content_selector.html().should.be.exactly("test content"); + this.plot.loader.progress_selector.classed("lz-loader-progress-animated").should.be.false(); + this.plot.loader.progress_selector.style("width").should.be.exactly("15%"); + this.plot.loader.update("still loading...", 62); + this.plot.loader.content_selector.html().should.be.exactly("still loading..."); + this.plot.loader.progress_selector.style("width").should.be.exactly("62%"); + this.plot.loader.setPercentCompleted(200); + this.plot.loader.progress_selector.style("width").should.be.exactly("100%"); + this.plot.loader.setPercentCompleted(-43); + this.plot.loader.progress_selector.style("width").should.be.exactly("1%"); + this.plot.loader.setPercentCompleted("foo"); + this.plot.loader.progress_selector.style("width").should.be.exactly("1%"); + }); + }); + }); diff --git a/test/Panel.js b/test/Panel.js index 7b733d60..35b9877c 100644 --- a/test/Panel.js +++ b/test/Panel.js @@ -166,40 +166,95 @@ describe('LocusZoom.Panel', function(){ }); }); - describe("SVG Composition", function() { - describe("Curtain", function() { - beforeEach(function(){ - d3.select("body").append("div").attr("id", "instance_id"); - this.instance = LocusZoom.populate("#instance_id"); - }); - it('last child of each panel container should be a curtain element', function(){ - Object.keys(this.instance.panels).forEach(function(panel_id){ - d3.select(this.instance.panels[panel_id].svg.group.node().parentNode.lastChild).attr("id").should.be.exactly("instance_id." + panel_id + ".curtain"); - d3.select(this.instance.panels[panel_id].svg.group.node().parentNode.lastChild).attr("class").should.be.exactly("lz-curtain"); - }.bind(this)); - }); - it('each panel should have a curtain object with stored svg selector', function(){ - Object.keys(this.instance.panels).forEach(function(panel_id){ - this.instance.panels[panel_id].curtain.should.be.an.Object; - this.instance.panels[panel_id].curtain.svg.should.be.an.Object; - assert.equal(this.instance.panels[panel_id].curtain.svg.html(), this.instance.svg.select("#instance_id\\." + panel_id + "\\.curtain").html()); - }.bind(this)); - }); - it('each panel curtain should have a method that drops the curtain', function(){ - Object.keys(this.instance.panels).forEach(function(panel_id){ - this.instance.panels[panel_id].curtain.drop.should.be.a.Function; - this.instance.panels[panel_id].curtain.drop(); - assert.equal(this.instance.panels[panel_id].curtain.svg.style("display"), ""); - }.bind(this)); - }); - it('each panel curtain should have a method that raises the curtain', function(){ - Object.keys(this.instance.panels).forEach(function(panel_id){ - this.instance.panels[panel_id].curtain.raise.should.be.a.Function; - this.instance.panels[panel_id].curtain.drop(); - this.instance.panels[panel_id].curtain.raise(); - assert.equal(this.instance.panels[panel_id].curtain.svg.style("display"), "none"); - }.bind(this)); - }); + describe("Panel Curtain and Loader", function() { + beforeEach(function(){ + var datasources = new LocusZoom.DataSources(); + this.layout = { + width: 100, + height: 100, + min_width: 100, + min_height: 100, + resizable: false, + aspect_ratio: 1, + panels: [ + { id: "test", + width: 100, + height: 100 } + ], + controls: false + }; + d3.select("body").append("div").attr("id", "plot"); + this.plot = LocusZoom.populate("#plot", datasources, this.layout); + this.panel = this.plot.panels.test; + }); + it("should have a curtain object with show/update/hide methods, a showing boolean, and selectors", function(){ + this.panel.should.have.property("curtain").which.is.an.Object; + this.panel.curtain.should.have.property("showing").which.is.exactly(false); + this.panel.curtain.should.have.property("show").which.is.a.Function; + this.panel.curtain.should.have.property("update").which.is.a.Function; + this.panel.curtain.should.have.property("hide").which.is.a.Function; + this.panel.curtain.should.have.property("selector").which.is.exactly(null); + this.panel.curtain.should.have.property("content_selector").which.is.exactly(null); + }); + it("should show/hide/update on command and track shown status", function(){ + this.panel.curtain.showing.should.be.false(); + this.panel.curtain.should.have.property("selector").which.is.exactly(null); + this.panel.curtain.should.have.property("content_selector").which.is.exactly(null); + this.panel.curtain.show("test content"); + this.panel.curtain.showing.should.be.true(); + this.panel.curtain.selector.empty().should.be.false(); + this.panel.curtain.content_selector.empty().should.be.false(); + this.panel.curtain.content_selector.html().should.be.exactly("test content"); + this.panel.curtain.hide(); + this.panel.curtain.showing.should.be.false(); + this.panel.curtain.should.have.property("selector").which.is.exactly(null); + this.panel.curtain.should.have.property("content_selector").which.is.exactly(null); + }); + it("should have a loader object with show/update/animate/setPercentCompleted/hide methods, a showing boolean, and selectors", function(){ + this.panel.should.have.property("loader").which.is.an.Object; + this.panel.loader.should.have.property("showing").which.is.exactly(false); + this.panel.loader.should.have.property("show").which.is.a.Function; + this.panel.loader.should.have.property("update").which.is.a.Function; + this.panel.loader.should.have.property("animate").which.is.a.Function; + this.panel.loader.should.have.property("update").which.is.a.Function; + this.panel.loader.should.have.property("setPercentCompleted").which.is.a.Function; + this.panel.loader.should.have.property("selector").which.is.exactly(null); + this.panel.loader.should.have.property("content_selector").which.is.exactly(null); + this.panel.loader.should.have.property("progress_selector").which.is.exactly(null); + }); + it("should show/hide/update on command and track shown status", function(){ + this.panel.loader.showing.should.be.false(); + this.panel.loader.should.have.property("selector").which.is.exactly(null); + this.panel.loader.should.have.property("content_selector").which.is.exactly(null); + this.panel.loader.should.have.property("progress_selector").which.is.exactly(null); + this.panel.loader.show("test content"); + this.panel.loader.showing.should.be.true(); + this.panel.loader.selector.empty().should.be.false(); + this.panel.loader.content_selector.empty().should.be.false(); + this.panel.loader.content_selector.html().should.be.exactly("test content"); + this.panel.loader.progress_selector.empty().should.be.false(); + this.panel.loader.hide(); + this.panel.loader.showing.should.be.false(); + this.panel.loader.should.have.property("selector").which.is.exactly(null); + this.panel.loader.should.have.property("content_selector").which.is.exactly(null); + this.panel.loader.should.have.property("progress_selector").which.is.exactly(null); + }); + it("should allow for animating or showing discrete percentages of completion", function(){ + this.panel.loader.show("test content").animate(); + this.panel.loader.progress_selector.classed("lz-loader-progress-animated").should.be.true(); + this.panel.loader.setPercentCompleted(15); + this.panel.loader.content_selector.html().should.be.exactly("test content"); + this.panel.loader.progress_selector.classed("lz-loader-progress-animated").should.be.false(); + this.panel.loader.progress_selector.style("width").should.be.exactly("15%"); + this.panel.loader.update("still loading...", 62); + this.panel.loader.content_selector.html().should.be.exactly("still loading..."); + this.panel.loader.progress_selector.style("width").should.be.exactly("62%"); + this.panel.loader.setPercentCompleted(200); + this.panel.loader.progress_selector.style("width").should.be.exactly("100%"); + this.panel.loader.setPercentCompleted(-43); + this.panel.loader.progress_selector.style("width").should.be.exactly("1%"); + this.panel.loader.setPercentCompleted("foo"); + this.panel.loader.progress_selector.style("width").should.be.exactly("1%"); }); });