1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/955e973c5b33/ Changeset: 955e973c5b33 User: carlfeberhard Date: 2014-02-14 16:17:56 Summary: UI: add pagination jquery plugin able to handle known/unknown data totals; Scatterplot: use that plugin for pagination, add class for highlighted data points Affected #: 9 files diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js @@ -5,7 +5,7 @@ Better separation of AJAX in scatterplot.js (maybe pass in function?) Labels should auto fill in chart control when dataset has column_names Allow column selection/config using the peek output as a base for UI - Allow setting perPage in config + Allow setting perPage in chart controls Allow option to auto set width/height based on screen real estate avail. Handle large number of pages better (Known genes hg19) Use d3.nest to allow grouping, pagination/filtration by group (e.g. chromCol) @@ -243,7 +243,7 @@ alert( 'Error loading data:\n' + xhr.responseText ); }) .then( function(){ - editor.render(); + editor.display.render(); }); }, diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/src/scatterplot-display.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -11,25 +11,15 @@ initialize : function( attributes ){ this.data = null, this.dataset = attributes.dataset; - this.calcNumPages(); - }, - - calcNumPages : function(){ - var config = this.model.get( 'config' ); - this.lineCount = this.dataset.metadata_data_lines, - this.numPages = ( this.lineCount )?( Math.ceil( this.lineCount / config.pagination.perPage ) ):( undefined ); - if( !this.lineCount || this.numPages === undefined ){ - console.warn( 'no data total found' ); - } + this.lineCount = this.dataset.metadata_data_lines || null; }, fetchData : function(){ -//TODO: doesn't work bc it's rendered in render()... - this.showLoadingIndicator( 'getting data' ); + this.showLoadingIndicator(); //console.debug( 'currPage', this.config.pagination.currPage ); var view = this, config = this.model.get( 'config' ), -//TODO: very tied to datasets - should be generalized eventually + //TODO: very tied to datasets - should be generalized eventually xhr = jQuery.getJSON( '/api/datasets/' + this.dataset.id, { data_type : 'raw_data', provider : 'dataset-column', @@ -37,6 +27,7 @@ offset : ( config.pagination.currPage * config.pagination.perPage ) }); xhr.done( function( data ){ + // no need to hide loading indicator, line info will write over that view.data = data.data; view.trigger( 'data:fetched', view ); view.renderData(); @@ -63,6 +54,7 @@ var html = [ '<div class="controls clear">', '<div class="left">', + '<div class="page-control"></div>', '</div>', '<div class="right">', '<p class="scatterplot-data-info"></p>', @@ -92,11 +84,24 @@ }, renderLeftControls : function(){ - if( this.lineCount ){ - this.$el.find( '.controls .left' ).empty().append( this.renderPagination() ); - } else { - this.$el.find( '.controls .left' ).empty().append( this.renderPrevNext() ); - } + var display = this, + config = this.model.get( 'config' ); + + this.$el.find( '.controls .left .page-control' ).pagination({ + startingPage : config.pagination.currPage, + perPage : config.pagination.perPage, + totalDataSize: this.lineCount, + currDataSize : this.data.length + + //TODO: move to named function and remove only named + }).off().on( 'pagination.page-change', function( event, page ){ + //console.debug( 'pagination:page-change', page ); + config.pagination.currPage = page; + display.model.set( 'config', { pagination: config.pagination }); + //console.debug( pagination, display.model.get( 'config' ).pagination ); + display.resetZoom(); + display.fetchData(); + }); return this; }, @@ -155,83 +160,6 @@ return this; }, - // ------------------------------------------------------------------------ data pagination -//TODO: to pagination control - goToPage : function( page ){ - var pagination = this.model.get( 'config' ).pagination; - //console.debug( 'goToPage', page, pagination, this.numPages ); - if( page <= 0 ){ page = 0; } - if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; } - if( page === pagination.currPage ){ return this; } - - //console.debug( '\t going to page ' + page ) - pagination.currPage = page; - this.model.set( 'config', { pagination: pagination }); - this.resetZoom(); - this.fetchData(); - return this; - }, - - nextPage : function(){ - var currPage = this.model.get( 'config' ).pagination.currPage; - return this.goToPage( currPage + 1 ); - }, - - prevPage : function(){ - var currPage = this.model.get( 'config' ).pagination.currPage; - return this.goToPage( currPage - 1 ); - }, - - /** render previous and next pagination buttons */ - renderPrevNext : function(){ - var config = this.model.get( 'config' ); - // if there's no data or there's less than one page of data - return null - if( !this.data ){ return null; } - if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; } - - var view = this, - $prev = $( '<li><a href="javascript:void(0);">Prev</a></li>' ) - .click( function(){ view.prevPage(); }), - $next = $( '<li><a href="javascript:void(0);">Next</a></li>' ) - .click( function(){ view.nextPage(); }); - - // disable if it either end - if( config.pagination.currPage === 0 ){ - $prev.addClass( 'disabled' ); - } - if( this.numPages && config.pagination.currPage === ( this.numPages - 1 ) ){ - $next.addClass( 'disabled' ); - } - return $( '<ul/>' ).addClass( 'pagination data-prev-next' ).append([ $prev, $next ]); - }, - - /** render page links for each possible page (if we can) */ - renderPagination : function(){ - var config = this.model.get( 'config' ); - // if there's no data, no page count, or there's less than one page of data - return null - if( !this.data ){ return null; } - if( !this.numPages ){ return null; } - if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; } - - var view = this, - $pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' ); - pageNumClick = function( ev ){ - view.goToPage( $( this ).data( 'page' ) ); - }; - - for( var i=0; i<this.numPages; i+=1 ){ - // add html5 data tag 'page' for later click event handler use - var $pageLi = $([ '<li><a href="javascript:void(0);">', i + 1, '</a></li>' ].join( '' )) - .attr( 'data-page', i ).click( pageNumClick ); - // highlight the current page - if( i === config.pagination.currPage ){ - $pageLi.addClass( 'active' ); - } - $pagesList.append( $pageLi ); - } - return $pagesList; - }, - // ------------------------------------------------------------------------ statistics display /** create a webworker to calc stats for data given */ getStats : function(){ diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/src/scatterplot.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js @@ -237,6 +237,7 @@ datapoints.on( 'mouseover', function( d, i ){ var datapoint = d3.select( this ); datapoint + .classed( 'highlight', true ) .style( 'fill', 'red' ) .style( 'fill-opacity', 1 ); @@ -273,6 +274,7 @@ datapoints.on( 'mouseout', function(){ // return the point to normal, remove hoverlines and info box d3.select( this ) + .classed( 'highlight', false ) .style( 'fill', 'black' ) .style( 'fill-opacity', 0.2 ); content.selectAll( '.hoverline' ).remove(); diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/static/scatterplot-edit.js --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -1,1 +1,1 @@ -function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.x.ticks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.y.ticks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("class","axis-label").text(b.x.label).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("class","axis-label").text(b.y.label).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="'+i((f=b.x,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="'+i((f=b.y,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n <option value="',(d=c.index)?d=d.call(a,{hash:{},data:b}):(d=a.index,d=typeof d===j?d.apply(a):d),e+=k(d)+'">',(d=c.name)?d=d.call(a,{hash:{},data:b}):(d=a.name,d=typeof d===j?d.apply(a):d),e+=k(d)+"</option>\n "}function g(){return'checked="true"'}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var h,i="",j="function",k=this.escapeExpression,l=this;return i+='<p class="help-text">\n Use the following controls to change the data used by the chart.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n\n<div class="column-select">\n <label>Data column for X: </label>\n <select name="xColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n<div class="column-select">\n <label>Data column for Y: </label>\n <select name="yColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="include-id">\n <label for="include-id-checkbox">Include a third column as data point IDs?</label>\n <input type="checkbox" name="include-id" id="include-id-checkbox" />\n <p class="help-text-small">\n These will be displayed (along with the x and y values) when you hover over\n a data point.\n </p>\n</div>\n<div class="column-select" style="display: none">\n <label for="ID-select">Data column for IDs: </label>\n <select name="idColumn">\n ',h=c.each.call(b,b.allColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="first-line-header" style="display: none;">\n <p>Possible headers: ',(h=c.possibleHeaders)?h=h.call(b,{hash:{},data:e}):(h=b.possibleHeaders,h=typeof h===j?h.apply(b):h),i+=k(h)+'\n </p>\n <label for="first-line-header-checkbox">Use the above as column headers?</label>\n <input type="checkbox" name="include-id" id="first-line-header-checkbox"\n ',h=c["if"].call(b,b.usePossibleHeaders,{hash:{},inverse:l.noop,fn:l.program(3,g,e),data:e}),(h||0===h)&&(i+=h),i+='/>\n <p class="help-text-small">\n It looks like Galaxy couldn\'t get proper column headers for this data.\n Would you like to use the column headers above as column names to select columns?\n </p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),this.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.log("dataset:",this.dataset),this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.dataset,d=c.metadata_column_names||[],e=this.model.get("config"),f=[],g=_.map(c.metadata_column_types,function(a,b){var c={index:b,type:a,name:d[b]||"column "+(b+1)};return("int"===c.type||"float"===c.type)&&f.push(c),c});f.length<2&&(f=g);var h=a.find(".tab-pane#data-control");h.html(ScatterplotConfigEditor.templates.dataControl({allColumns:g,numericColumns:f}));var i={xColumn:_.isFinite(e.xColumn)?e.xColumn:f[0].index,yColumn:_.isFinite(e.yColumn)?e.yColumn:f[1].index,idColumn:g[0].index};if(_.isFinite(e.idColumn))i.idColumn=e.idColumn;else if(g.length>2){var j=_.find(g,function(a,b){return b!==i.xColumn&&b!==i.yColumn});i.idColumn=j.index}return e=this.model.set("config",i,{silent:!0}).get("config"),h.find('[name="xColumn"]').val(e.xColumn).on("change",function(){b.model.set("config",{xColumn:Number($(this).val())})}),h.find('[name="yColumn"]').val(e.yColumn).on("change",function(){b.model.set("config",{yColumn:Number($(this).val())})}),h.find('select[name="idColumn"]').val(e.idColumn).on("change",function(){b.model.set("config",{idColumn:Number($(this).val())})}),void 0!==e.idColumn&&h.find("#include-id-checkbox").prop("checked",!0).trigger("change"),h.find("[title]").tooltip(),h},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.calcNumPages()},calcNumPages:function(){var a=this.model.get("config");this.lineCount=this.dataset.metadata_data_lines,this.numPages=this.lineCount?Math.ceil(this.lineCount/a.pagination.perPage):void 0,this.lineCount&&void 0!==this.numPages||console.warn("no data total found")},fetchData:function(){this.showLoadingIndicator("getting data");var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="left">',"</div>",'<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){return this.lineCount?this.$el.find(".controls .left").empty().append(this.renderPagination()):this.$el.find(".controls .left").empty().append(this.renderPrevNext()),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},goToPage:function(a){var b=this.model.get("config").pagination;return 0>=a&&(a=0),this.numPages&&a>=this.numPages&&(a=this.numPages-1),a===b.currPage?this:(b.currPage=a,this.model.set("config",{pagination:b}),this.resetZoom(),this.fetchData(),this)},nextPage:function(){var a=this.model.get("config").pagination.currPage;return this.goToPage(a+1)},prevPage:function(){var a=this.model.get("config").pagination.currPage;return this.goToPage(a-1)},renderPrevNext:function(){var a=this.model.get("config");if(!this.data)return null;if(0===a.pagination.currPage&&this.data.length<a.pagination.perPage)return null;var b=this,c=$('<li><a href="javascript:void(0);">Prev</a></li>').click(function(){b.prevPage()}),d=$('<li><a href="javascript:void(0);">Next</a></li>').click(function(){b.nextPage()});return 0===a.pagination.currPage&&c.addClass("disabled"),this.numPages&&a.pagination.currPage===this.numPages-1&&d.addClass("disabled"),$("<ul/>").addClass("pagination data-prev-next").append([c,d])},renderPagination:function(){var a=this.model.get("config");if(!this.data)return null;if(!this.numPages)return null;if(0===a.pagination.currPage&&this.data.length<a.pagination.perPage)return null;var b=this,c=$("<ul/>").addClass("pagination data-pages");pageNumClick=function(){b.goToPage($(this).data("page"))};for(var d=0;d<this.numPages;d+=1){var e=$(['<li><a href="javascript:void(0);">',d+1,"</a></li>"].join("")).attr("data-page",d).click(pageNumClick);d===a.pagination.currPage&&e.addClass("active"),c.append(e)}return c},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.x.label,e=b.y.label,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file +function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.x.ticks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.y.ticks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("class","axis-label").text(b.x.label).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("class","axis-label").text(b.y.label).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).classed("highlight",!1).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="'+i((f=b.x,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="'+i((f=b.y,f=null==f||f===!1?f:f.label,typeof f===h?f.apply(b):f))+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n <option value="',(d=c.index)?d=d.call(a,{hash:{},data:b}):(d=a.index,d=typeof d===j?d.apply(a):d),e+=k(d)+'">',(d=c.name)?d=d.call(a,{hash:{},data:b}):(d=a.name,d=typeof d===j?d.apply(a):d),e+=k(d)+"</option>\n "}function g(){return'checked="true"'}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var h,i="",j="function",k=this.escapeExpression,l=this;return i+='<p class="help-text">\n Use the following controls to change the data used by the chart.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n\n<div class="column-select">\n <label>Data column for X: </label>\n <select name="xColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n<div class="column-select">\n <label>Data column for Y: </label>\n <select name="yColumn">\n ',h=c.each.call(b,b.numericColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="include-id">\n <label for="include-id-checkbox">Include a third column as data point IDs?</label>\n <input type="checkbox" name="include-id" id="include-id-checkbox" />\n <p class="help-text-small">\n These will be displayed (along with the x and y values) when you hover over\n a data point.\n </p>\n</div>\n<div class="column-select" style="display: none">\n <label for="ID-select">Data column for IDs: </label>\n <select name="idColumn">\n ',h=c.each.call(b,b.allColumns,{hash:{},inverse:l.noop,fn:l.program(1,f,e),data:e}),(h||0===h)&&(i+=h),i+='\n </select>\n</div>\n\n\n<div id="first-line-header" style="display: none;">\n <p>Possible headers: ',(h=c.possibleHeaders)?h=h.call(b,{hash:{},data:e}):(h=b.possibleHeaders,h=typeof h===j?h.apply(b):h),i+=k(h)+'\n </p>\n <label for="first-line-header-checkbox">Use the above as column headers?</label>\n <input type="checkbox" name="include-id" id="first-line-header-checkbox"\n ',h=c["if"].call(b,b.usePossibleHeaders,{hash:{},inverse:l.noop,fn:l.program(3,g,e),data:e}),(h||0===h)&&(i+=h),i+='/>\n <p class="help-text-small">\n It looks like Galaxy couldn\'t get proper column headers for this data.\n Would you like to use the column headers above as column names to select columns?\n </p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),this.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.log("dataset:",this.dataset),this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.dataset,d=c.metadata_column_names||[],e=this.model.get("config"),f=[],g=_.map(c.metadata_column_types,function(a,b){var c={index:b,type:a,name:d[b]||"column "+(b+1)};return("int"===c.type||"float"===c.type)&&f.push(c),c});f.length<2&&(f=g);var h=a.find(".tab-pane#data-control");h.html(ScatterplotConfigEditor.templates.dataControl({allColumns:g,numericColumns:f}));var i={xColumn:_.isFinite(e.xColumn)?e.xColumn:f[0].index,yColumn:_.isFinite(e.yColumn)?e.yColumn:f[1].index,idColumn:g[0].index};if(_.isFinite(e.idColumn))i.idColumn=e.idColumn;else if(g.length>2){var j=_.find(g,function(a,b){return b!==i.xColumn&&b!==i.yColumn});i.idColumn=j.index}return e=this.model.set("config",i,{silent:!0}).get("config"),h.find('[name="xColumn"]').val(e.xColumn).on("change",function(){b.model.set("config",{xColumn:Number($(this).val())})}),h.find('[name="yColumn"]').val(e.yColumn).on("change",function(){b.model.set("config",{yColumn:Number($(this).val())})}),h.find('select[name="idColumn"]').val(e.idColumn).on("change",function(){b.model.set("config",{idColumn:Number($(this).val())})}),void 0!==e.idColumn&&h.find("#include-id-checkbox").prop("checked",!0).trigger("change"),h.find("[title]").tooltip(),h},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.calcNumPages()},calcNumPages:function(){var a=this.model.get("config");this.lineCount=this.dataset.metadata_data_lines,this.numPages=this.lineCount?Math.ceil(this.lineCount/a.pagination.perPage):void 0,this.lineCount&&void 0!==this.numPages||console.warn("no data total found")},fetchData:function(){this.showLoadingIndicator("getting data");var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="left">','<div class="page-control"></div>',"</div>",'<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount||null,currDataSize:this.data.length,maxWidth:540}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.x.label,e=b.y.label,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},x:{ticks:10,label:"X"},y:{ticks:10,label:"Y"},datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/static/scatterplot.css --- a/config/plugins/visualizations/scatterplot/static/scatterplot.css +++ b/config/plugins/visualizations/scatterplot/static/scatterplot.css @@ -125,13 +125,9 @@ margin: 0px; } -.scatterplot-display .controls .left { - float: left; -} - -.scatterplot-display .controls .pagination { - display: inline-block; - margin: 0px; +.scatterplot-display .pagination-scroll-container { + /* float left makes this not collapse very well */ + max-width: 540px; } .scatterplot-display .controls .data-prev-next { @@ -139,22 +135,16 @@ margin: 0px 4px 0px 0px; } -.scatterplot-display .controls .pagination-container { - display: inline-block; - overflow: auto; - max-width: 256px; - /*very tweaky*/ - margin: 0px 8px 0px 0px; - height: 27px; - padding-top: 5px; +.scatterplot-display .controls { } -.scatterplot-display .controls .pagination-container a { - display: inline; - /* WTH, bootstrap? */ - position: static; - float: none; - /* up from 4px -> 5px */ - padding: 5px 10px 5px 10px; + +.scatterplot-display .controls .left, +.scatterplot-display .controls .right { + position: relative; +} + +.scatterplot-display .controls .left { + float: left; } .scatterplot-display .controls .right { diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -32,6 +32,7 @@ <script type="text/javascript" src="/static/scripts/libs/d3.js"></script><script type="text/javascript" src="/static/scripts/mvc/base-mvc.js"></script> +<script type="text/javascript" src="/static/scripts/mvc/ui.js"></script><script type="text/javascript" src="/static/scripts/mvc/visualization/visualization-model.js"></script><script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot-edit.js"></script> diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 static/scripts/mvc/ui.js --- a/static/scripts/mvc/ui.js +++ b/static/scripts/mvc/ui.js @@ -517,6 +517,90 @@ //============================================================================== +function LoadingIndicator( $where, options ){ +//TODO: move out of global +//TODO: too specific to history panel + + var self = this; + // defaults + options = jQuery.extend({ + cover : false + }, options || {} ); + + function render(){ + var html = [ + '<div class="loading-indicator">', + '<div class="loading-indicator-text">', + '<span class="fa fa-spinner fa-spin fa-lg"></span>', + '<span class="loading-indicator-message">loading...</span>', + '</div>', + '</div>' + ].join( '\n' ); + + var $indicator = $( html ).hide().css( options.css || { + position : 'fixed' + }), + $text = $indicator.children( '.loading-indicator-text' ); + + if( options.cover ){ + $indicator.css({ + 'z-index' : 2, + top : $where.css( 'top' ), + bottom : $where.css( 'bottom' ), + left : $where.css( 'left' ), + right : $where.css( 'right' ), + opacity : 0.5, + 'background-color': 'white', + 'text-align': 'center' + }); + $text = $indicator.children( '.loading-indicator-text' ).css({ + 'margin-top' : '20px' + }); + + } else { + $text = $indicator.children( '.loading-indicator-text' ).css({ + margin : '12px 0px 0px 10px', + opacity : '0.85', + color : 'grey' + }); + $text.children( '.loading-indicator-message' ).css({ + margin : '0px 8px 0px 0px', + 'font-style' : 'italic' + }); + } + return $indicator; + } + + self.show = function( msg, speed, callback ){ + msg = msg || 'loading...'; + speed = speed || 'fast'; + // since position is fixed - we insert as sibling + self.$indicator = render().insertBefore( $where ); + self.message( msg ); + self.$indicator.fadeIn( speed, callback ); + return self; + }; + + self.message = function( msg ){ + self.$indicator.find( 'i' ).text( msg ); + }; + + self.hide = function( speed, callback ){ + speed = speed || 'fast'; + if( self.$indicator && self.$indicator.size() ){ + self.$indicator.fadeOut( speed, function(){ + self.$indicator.remove(); + if( callback ){ callback(); } + }); + } else { + if( callback ){ callback(); } + } + return self; + }; + return self; +} + +//============================================================================== (function(){ /** searchInput: (jQuery plugin) * Creates a search input, a clear button, and loading indicator @@ -614,7 +698,7 @@ clearSearchInput.call( this, event ); }); } - + // .................................................................... loadingIndicator rendering // a button for clearing the search bar, placed on the right hand side function $loadingIndicator(){ @@ -803,91 +887,6 @@ //============================================================================== -function LoadingIndicator( $where, options ){ -//TODO: move out of global -//TODO: too specific to history panel - - var self = this; - // defaults - options = jQuery.extend({ - cover : false - }, options || {} ); - - function render(){ - var html = [ - '<div class="loading-indicator">', - '<div class="loading-indicator-text">', - '<span class="fa fa-spinner fa-spin fa-lg"></span>', - '<span class="loading-indicator-message">loading...</span>', - '</div>', - '</div>' - ].join( '\n' ); - - var $indicator = $( html ).hide().css( options.css || { - position : 'fixed' - }), - $text = $indicator.children( '.loading-indicator-text' ); - - if( options.cover ){ - $indicator.css({ - 'z-index' : 2, - top : $where.css( 'top' ), - bottom : $where.css( 'bottom' ), - left : $where.css( 'left' ), - right : $where.css( 'right' ), - opacity : 0.5, - 'background-color': 'white', - 'text-align': 'center' - }); - $text = $indicator.children( '.loading-indicator-text' ).css({ - 'margin-top' : '20px' - }); - - } else { - $text = $indicator.children( '.loading-indicator-text' ).css({ - margin : '12px 0px 0px 10px', - opacity : '0.85', - color : 'grey' - }); - $text.children( '.loading-indicator-message' ).css({ - margin : '0px 8px 0px 0px', - 'font-style' : 'italic' - }); - } - return $indicator; - } - - self.show = function( msg, speed, callback ){ - msg = msg || 'loading...'; - speed = speed || 'fast'; - // since position is fixed - we insert as sibling - self.$indicator = render().insertBefore( $where ); - self.message( msg ); - self.$indicator.fadeIn( speed, callback ); - return self; - }; - - self.message = function( msg ){ - self.$indicator.find( 'i' ).text( msg ); - }; - - self.hide = function( speed, callback ){ - speed = speed || 'fast'; - if( self.$indicator && self.$indicator.size() ){ - self.$indicator.fadeOut( speed, function(){ - self.$indicator.remove(); - if( callback ){ callback(); } - }); - } else { - if( callback ){ callback(); } - } - return self; - }; - return self; -} - - -//============================================================================== /** * Template function that produces a bootstrap dropdown to replace the * vanilla HTML select input. Pass in an array of options and an initial selection: @@ -1082,3 +1081,221 @@ } }); }()); + + +//============================================================================== +(function(){ + /** Builds (twitter bootstrap styled) pagination controls. + * If the totalDataSize is not null, a horizontal list of page buttons is displayed. + * If totalDataSize is null, two links ('Prev' and 'Next) are displayed. + * When pages are changed, a 'pagination.page-change' event is fired + * sending the event and the (0-based) page requested. + */ + function Pagination( element, options ){ + /** the total number of pages */ + this.numPages = null; + /** the current, active page */ + this.currPage = 0; + return this.init( element, options ); + } + + /** data key under which this object will be stored in the element */ + Pagination.prototype.DATA_KEY = 'pagination'; + /** default options */ + Pagination.prototype.defaults = { + /** which page to begin at */ + startingPage : 0, + /** number of data per page */ + perPage : 20, + /** the total number of data (null == unknown) */ + totalDataSize : null, + /** size of current data on current page */ + currDataSize : null + }; + + /** init the control, calc numPages if possible, and render + * @param {jQuery} the element that will contain the pagination control + * @param {Object} options a map containing overrides to the pagination default options + */ + Pagination.prototype.init = function _init( $element, options ){ + options = options || {}; + this.$element = $element; + this.options = jQuery.extend( true, {}, this.defaults, options ); + + this.currPage = this.options.startingPage; + if( this.options.totalDataSize !== null ){ + this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage ); + // limit currPage by numPages + if( this.currPage >= this.numPages ){ + this.currPage = this.numPages - 1; + } + } + //console.debug( 'Pagination.prototype.init:', this.$element, this.currPage ); + //console.debug( JSON.stringify( this.options ) ); + + // bind to data of element + this.$element.data( Pagination.prototype.DATA_KEY, this ); + + this._render(); + return this; + }; + + /** helper to create a simple li + a combo */ + function _make$Li( contents ){ + return $([ + '<li><a href="javascript:void(0);">', contents, '</a></li>' + ].join( '' )); + } + + /** render previous and next pagination buttons */ + Pagination.prototype._render = function __render(){ + // no data - no pagination + if( this.options.totalDataSize === 0 ){ return this; } + // only one page + if( this.numPages === 1 ){ return this; } + + // when the number of pages are known, render each page as a link + if( this.numPages > 0 ){ + this._renderPages(); + this._scrollToActivePage(); + + // when the number of pages is not known, render previous or next + } else { + this._renderPrevNext(); + } + return this; + }; + + /** render previous and next pagination buttons */ + Pagination.prototype._renderPrevNext = function __renderPrevNext(){ + var pagination = this, + $prev = _make$Li( 'Prev' ), + $next = _make$Li( 'Next' ), + $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' ); + + // disable if it either end + if( this.currPage === 0 ){ + $prev.addClass( 'disabled' ); + } else { + $prev.click( function(){ pagination.prevPage(); }); + } + if( ( this.numPages && this.currPage === ( this.numPages - 1 ) ) + || ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){ + $next.addClass( 'disabled' ); + } else { + $next.click( function(){ pagination.nextPage(); }); + } + + this.$element.html( $paginationContainer.append([ $prev, $next ]) ); + //console.debug( this.$element, this.$element.html() ); + return this.$element; + }; + + /** render page links for each possible page (if we can) */ + Pagination.prototype._renderPages = function __renderPages(){ + // it's better to scroll the control and let the user see all pages + // than to force her/him to change pages in order to find the one they want (as traditional << >> does) + var pagination = this, + $scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ), + $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ), + page$LiClick = function( ev ){ + pagination.goToPage( $( this ).data( 'page' ) ); + }; + + for( var i=0; i<this.numPages; i+=1 ){ + // add html5 data tag 'page' for later click event handler use + var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick ); + // highlight the current page + if( i === this.currPage ){ + $pageLi.addClass( 'active' ); + } + //console.debug( '\t', $pageLi ); + $paginationContainer.append( $pageLi ); + } + return this.$element.html( $scrollingContainer.html( $paginationContainer ) ); + }; + + /** scroll scroll-container (if any) to show the active page */ + Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){ + // scroll to show active page in center of scrollable area + var $container = this.$element.find( '.pagination-scroll-container' ); + // no scroll container : don't scroll + if( !$container.size() ){ return this; } + + var $activePage = this.$element.find( 'li.active' ), + midpoint = $container.width() / 2; + //console.debug( $container, $activePage, midpoint ); + $container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint ); + return this; + }; + + /** go to a certain page */ + Pagination.prototype.goToPage = function goToPage( page ){ + if( page <= 0 ){ page = 0; } + if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; } + if( page === this.currPage ){ return this; } + + //console.debug( '\t going to page ' + page ) + this.currPage = page; + this.$element.trigger( 'pagination.page-change', this.currPage ); + //console.info( 'pagination:page-change', this.currPage ); + this._render(); + return this; + }; + + /** go to the previous page */ + Pagination.prototype.prevPage = function prevPage(){ + return this.goToPage( this.currPage - 1 ); + }; + + /** go to the next page */ + Pagination.prototype.nextPage = function nextPage(){ + return this.goToPage( this.currPage + 1 ); + }; + + /** return the current page */ + Pagination.prototype.page = function page(){ + return this.currPage; + }; + + // alternate constructor invocation + Pagination.create = function _create( $element, options ){ + return new Pagination( $element, options ); + }; + + // as jq plugin + jQuery.fn.extend({ + pagination : function $pagination( options ){ + var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 ); + + // if passed an object - use that as an options map to create pagination for each selected + if( jQuery.type( options ) === 'object' ){ + return this.map( function(){ + Pagination.create( $( this ), options ); + return this; + }); + } + + // (other invocations only work on the first element in selected) + var $firstElement = $( this[0] ), + previousControl = $firstElement.data( Pagination.prototype.DATA_KEY ); + // if a pagination control was found for this element, either... + if( previousControl ){ + // invoke a function on the pagination object if passed a string (the function name) + if( jQuery.type( options ) === 'string' ){ + var fn = previousControl[ options ]; + if( jQuery.type( fn ) === 'function' ){ + return fn.apply( previousControl, nonOptionsArgs ); + } + + // if passed nothing, return the previously set control + } else { + return previousControl; + } + } + // if there is no control already set, return undefined + return undefined; + } + }); +}()); + diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 static/style/blue/base.css --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1390,6 +1390,12 @@ .quota-meter-bar-warn{background-color:#bf822c} .quota-meter-bar-error{background-color:#b93e3a} .quota-meter-text{position:absolute;top:50%;left:0;width:100px;height:16px;margin-top:-6px;text-align:center;z-index:9001;color:#000;white-space:nowrap} +.pagination{margin:0px} +.pagination-scroll-container{overflow:auto;background-color:#F8F8F8;border-radius:3px;border:1px solid #BFBFBF} +.pagination-scroll-container .pagination-page-list{margin:3px 0px 3px 0px} +.pagination-scroll-container .pagination-page-list>li:first-child>a,.pagination-scroll-container .pagination-page-list>li:first-child>span{border-radius:0px;border-left:0px} +.pagination-scroll-container .pagination-page-list>li:last-child>a,.pagination-scroll-container .pagination-page-list>li:last-child>span{border-radius:0px} +.pagination-scroll-container .pagination-page-list>li>a{float:none;position:static;border:1px solid #BFBFBF;border-width:0px 0px 0px 1px} div.metadataForm{border:solid #aaaaaa 1px} div.metadataFormTitle{font-weight:bold;padding:5px;padding-left:10px;padding-right:10px;background:#cccccc;background-repeat:repeat-x;background-position:top;border-bottom:solid #aaaaaa 1px} div.metadataFormBody{background:#FFFFFF;padding:5px 0} diff -r f199022e48088ab4fd71701e07899b729496b005 -r 955e973c5b33eba4e0e68979a7c20c0bd0558e37 static/style/src/less/base.less --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -508,6 +508,39 @@ white-space: nowrap; } +// ---------------------------------------------------------------------------- pagination & scrolling pagination +.pagination { + margin: 0px; +} + +.pagination-scroll-container { + overflow : auto; + background-color: #F8F8F8; + border-radius : 3px; + border : 1px solid #BFBFBF; +} + +.pagination-scroll-container .pagination-page-list { + margin : 3px 0px 3px 0px; +} + +.pagination-scroll-container .pagination-page-list > li:first-child > a, +.pagination-scroll-container .pagination-page-list > li:first-child > span { + border-radius : 0px; + border-left : 0px; +} + +.pagination-scroll-container .pagination-page-list > li:last-child > a, +.pagination-scroll-container .pagination-page-list > li:last-child > span { + border-radius : 0px; +} + +.pagination-scroll-container .pagination-page-list > li > a { + float : none; + position : static; + border : 1px solid #BFBFBF; + border-width : 0px 0px 0px 1px; +} // ==== Tool form styles ==== Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.