From 5d01f6393c63ad82e6a452bd0cb977a3b85ff629 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 14 Feb 2014 10:17:56 -0500 Subject: [PATCH] UI: add pagination jquery plugin able to handle known/unknown data totals; Scatterplot: use that plugin for pagination, add class for highlighted data points --- .../src/scatterplot-config-editor.js | 4 +- .../scatterplot/src/scatterplot-display.js | 118 ++---- .../scatterplot/src/scatterplot.js | 2 + .../scatterplot/static/scatterplot-edit.js | 2 +- .../scatterplot/static/scatterplot.css | 34 +- .../scatterplot/templates/scatterplot.mako | 1 + static/scripts/mvc/ui.js | 389 ++++++++++++++---- static/style/blue/base.css | 6 + static/style/src/less/base.less | 33 ++ 9 files changed, 383 insertions(+), 206 deletions(-) diff --git a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js index 1089f511a57..88e9c4aba00 100644 --- a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js @@ -5,7 +5,7 @@ todo: 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 @@ var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({ alert( 'Error loading data:\n' + xhr.responseText ); }) .then( function(){ - editor.render(); + editor.display.render(); }); }, diff --git a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js index ea9927430d4..adebdf4934a 100644 --- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -11,25 +11,15 @@ var ScatterplotDisplay = Backbone.View.extend({ 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 @@ var ScatterplotDisplay = Backbone.View.extend({ 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 ScatterplotDisplay = Backbone.View.extend({ var html = [ '
', '
', + '
', '
', '
', '

', @@ -92,11 +84,24 @@ var ScatterplotDisplay = Backbone.View.extend({ }, 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 @@ var ScatterplotDisplay = Backbone.View.extend({ 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 = $( '
  • Prev
  • ' ) - .click( function(){ view.prevPage(); }), - $next = $( '
  • Next
  • ' ) - .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 $( '
      ' ).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 = $( '
        ' ).addClass( 'pagination data-pages' ); - pageNumClick = function( ev ){ - view.goToPage( $( this ).data( 'page' ) ); - }; - - for( var i=0; i', i + 1, '' ].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 --git a/config/plugins/visualizations/scatterplot/src/scatterplot.js b/config/plugins/visualizations/scatterplot/src/scatterplot.js index e8c0660b5d1..fe4766d13f1 100644 --- a/config/plugins/visualizations/scatterplot/src/scatterplot.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js @@ -237,6 +237,7 @@ function scatterplot( renderTo, config, data ){ 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 @@ function scatterplot( renderTo, config, data ){ 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 --git a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js index c591a9317cf..59e2cc59b1b 100644 --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -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,$(['
        ',void 0!==b.idColumn?"
        "+d[b.idColumn]+"
        ":"","
        ",j(d),"
        ","
        ",k(d),"
        ","
        "].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")= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='

        \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

        \n\n
        \n \n
        ',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'
        \n
        \n

        \n Size of the graphic representation of each data point\n

        \n
        \n\n
        \n \n
        ',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'
        \n
        \n

        \n (not including chart margins and axes)\n

        \n
        \n\n
        \n \n
        ',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'
        \n
        \n

        \n (not including chart margins and axes)\n

        \n
        \n\n
        \n \n \n

        \n
        \n\n
        \n \n \n

        \n
        \n\n\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n \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+='

        \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

        \n\n\n
        \n \n \n
        \n
        \n \n \n
        \n\n\n
        \n \n \n

        \n These will be displayed (along with the x and y values) when you hover over\n a data point.\n

        \n
        \n\n\n\n\n\n\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+='
        \n \n \n\n \n
        \n \n
        \n \n
        \n \n \n
        \n \n
        \n\n \n
        \n\n
        \n
        \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(['
        ','','loading...',"
        "].join(""))},template:function(){var a=['
        ','
        ',"
        ",'
        ','

        ','','',"
        ","
        ","",'
        '].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.lengthPrev').click(function(){b.prevPage()}),d=$('
      • Next
      • ').click(function(){b.nextPage()});return 0===a.pagination.currPage&&c.addClass("disabled"),this.numPages&&a.pagination.currPage===this.numPages-1&&d.addClass("disabled"),$("
          ").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").addClass("pagination data-pages");pageNumClick=function(){b.goToPage($(this).data("page"))};for(var d=0;d',d+1,""].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=$("").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].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,$(['
          ',void 0!==b.idColumn?"
          "+d[b.idColumn]+"
          ":"","
          ",j(d),"
          ","
          ",k(d),"
          ","
          "].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")= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='

          \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

          \n\n
          \n \n
          ',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'
          \n
          \n

          \n Size of the graphic representation of each data point\n

          \n
          \n\n
          \n \n
          ',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'
          \n
          \n

          \n (not including chart margins and axes)\n

          \n
          \n\n
          \n \n
          ',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'
          \n
          \n

          \n (not including chart margins and axes)\n

          \n
          \n\n
          \n \n \n

          \n
          \n\n
          \n \n \n

          \n
          \n\n\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+='\n \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+='

          \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

          \n\n\n
          \n \n \n
          \n
          \n \n \n
          \n\n\n
          \n \n \n

          \n These will be displayed (along with the x and y values) when you hover over\n a data point.\n

          \n
          \n\n\n\n\n\n\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+='
          \n \n \n\n \n
          \n \n
          \n \n
          \n \n \n
          \n \n
          \n\n \n
          \n\n
          \n
          \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(['
          ','','loading...',"
          "].join(""))},template:function(){var a=['
          ','
          ','
          ',"
          ",'
          ','

          ','','',"
          ","
          ","",'
          '].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=$("
          ",d,"",e,"
          ",b,"",a[0],"",a[1],"
          ").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].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 --git a/config/plugins/visualizations/scatterplot/static/scatterplot.css b/config/plugins/visualizations/scatterplot/static/scatterplot.css index f3105d7568c..679dfe99347 100644 --- 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 --git a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako index 06d9a65b4e9..8688ec1c033 100644 --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -32,6 +32,7 @@ + diff --git a/static/scripts/mvc/ui.js b/static/scripts/mvc/ui.js index 2d8546d0e68..656dc939f52 100644 --- a/static/scripts/mvc/ui.js +++ b/static/scripts/mvc/ui.js @@ -516,6 +516,90 @@ var faIconButton = function( options ){ }; +//============================================================================== +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 = [ + '
          ', + '
          ', + '', + 'loading...', + '
          ', + '
          ' + ].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) @@ -614,7 +698,7 @@ var faIconButton = function( options ){ clearSearchInput.call( this, event ); }); } - + // .................................................................... loadingIndicator rendering // a button for clearing the search bar, placed on the right hand side function $loadingIndicator(){ @@ -802,91 +886,6 @@ var faIconButton = function( options ){ }()); -//============================================================================== -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 = [ - '
          ', - '
          ', - '', - 'loading...', - '
          ', - '
          ' - ].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 @@ -1082,3 +1081,221 @@ function dropDownSelect( options, selected ){ } }); }()); + + +//============================================================================== +(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 $([ + '
        • ', contents, '
        • ' + ].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 = $( '
            ' ).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 = $( '
            ' ).addClass( 'pagination-scroll-container' ), + $paginationContainer = $( '
              ' ).addClass( 'pagination pagination-page-list' ), + page$LiClick = function( ev ){ + pagination.goToPage( $( this ).data( 'page' ) ); + }; + + for( var i=0; i= 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 --git a/static/style/blue/base.css b/static/style/blue/base.css index f95abaedea5..9755cdcb2a4 100644 --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1390,6 +1390,12 @@ div.unified-panel-body{position:absolute;top:30px;bottom:0;width:100%} .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 --git a/static/style/src/less/base.less b/static/style/src/less/base.less index 934618ec9d0..62174e41003 100644 --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -508,6 +508,39 @@ div.unified-panel-body { 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 ====
          ",d,"",e,"
          ",b,"",a[0],"",a[1],"