commit/galaxy-central: carlfeberhard: Visualizations Registry: default to on
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/0d50efaac5d4/ Changeset: 0d50efaac5d4 User: carlfeberhard Date: 2014-01-14 16:04:20 Summary: Visualizations Registry: default to on Affected #: 16 files diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 config/plugins/visualizations/scatterplot/config/scatterplot.xml --- a/config/plugins/visualizations/scatterplot/config/scatterplot.xml +++ b/config/plugins/visualizations/scatterplot/config/scatterplot.xml @@ -7,9 +7,15 @@ <test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test><to_param param_attr="id">dataset_id</to_param></data_source> + <data_source> + <model_class>Visualization</model_class> + <test test_attr="type">scatterplot</test> + <to_param param_attr="id">visualization_id</to_param> + </data_source></data_sources><params><param type="dataset" var_name_in_template="hda" required="true">dataset_id</param> + <param type="visualization" var_name_in_template="visualization">visualization_id</param></params><template>scatterplot.mako</template></visualization> diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 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 @@ -10,6 +10,7 @@ Allow setting perPage of config Auto render if given data and/or config 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) Semantic HTML (figure, caption) Save as visualization, load from visualization @@ -29,26 +30,28 @@ * configuring which data will be used * configuring the plot display */ -var ScatterplotConfigEditor = BaseView.extend( LoggableMixin ).extend({ +var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({ //TODO: !should be a view on a visualization model //logger : console, className : 'scatterplot-control-form', /** initialize requires a configuration Object containing a dataset Object */ initialize : function( attributes ){ - //console.log( this + '.initialize, attributes:', attributes ); - if( !attributes || !attributes.config || !attributes.dataset ){ - throw new Error( "ScatterplotView requires a configuration and dataset" ); + if( !this.model ){ + this.model = new Visualization({ type: 'scatterplot' }); } - //console.log( 'config:', attributes.config ); + console.log( this + '.initialize, attributes:', attributes ); + if( !attributes || !attributes.dataset ){ + throw new Error( "ScatterplotConfigEditor requires a dataset" ); + } this.dataset = attributes.dataset; - //console.log( 'dataset:', this.dataset ); + console.log( 'dataset:', this.dataset ); //TODO: ScatterplotView -> ScatterplotDisplay, this.plotView -> this.display this.plotView = new ScatterplotView({ dataset : attributes.dataset, - config : attributes.config + model : this.model //TODO: if data }); }, @@ -58,8 +61,7 @@ //console.log( this + '.render' ); // render the tab controls, areas and loading indicator - this.$el.append( ScatterplotConfigEditor.templates.mainLayout({ - })); + this.$el.append( ScatterplotConfigEditor.templates.mainLayout({})); // render the tab content this.$el.find( '#data-control' ).append( this._render_dataControl() ); diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 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(){q.redraw(),e(),s=d(),$(".chart-info-box").remove(),$(o.node()).trigger("zoom.scatterplot",[])}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',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,10]),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=4;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",b.height).attr("r",0);t.transition().duration(b.animDuration).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",b.datapointSize),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.Templates=this.Templates||{},this.Templates.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.Templates.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.Templates.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 </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=BaseView.extend(LoggableMixin).extend({className:"scatterplot-control-form",initialize:function(a){if(!a||!a.config||!a.dataset)throw new Error("ScatterplotView requires a configuration and dataset");this.dataset=a.dataset,this.plotView=new ScatterplotView({dataset:a.dataset,config:a.config})},render:function(){return this.$el.append(ScatterplotConfigEditor.templates.mainLayout({})),this.$el.find("#data-control").append(this._render_dataControl()),this._render_chartControls(this.$el.find("#chart-control")),this._render_chartDisplay(),this.$el.find("[title]").tooltip(),this},_render_dataControl:function(){var a=this.dataset,b=_.map(a.metadata_column_types,function(b,c){var d={index:c,type:b,name:"column "+(c+1)};return a.metadata_column_names&&a.metadata_column_names[c]&&(d.name=a.metadata_column_names[c]),d}),c=_.filter(b,function(a){return"int"===a.type||"float"===a.type});2>c&&(c=b);var d=this.$el.find(".tab-pane#data-control");return d.html(ScatterplotConfigEditor.templates.dataControl({allColumns:b,numericColumns:c})),d.find('[name="xColumn"]').val(this.plotView.config.xColumn||c[0].index),d.find('[name="yColumn"]').val(this.plotView.config.yColumn||c[1].index),void 0!==this.plotView.config.idColumn&&(d.find("#include-id-checkbox").prop("checked",!0).trigger("change"),d.find('select[name="idColumn"]').val(this.plotView.config.idColumn)),d},_render_chartControls:function(a){function b(){var a=$(this);a.siblings(".slider-output").text(a.slider("value"))}a.html(ScatterplotConfigEditor.templates.chartControl(this.plotView.config));var c=this,d={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};return a.find(".numeric-slider-input").each(function(){var a=$(this),e=a.attr("data-config-key"),f=_.extend(d[e],{value:c.plotView.config[e],change:b,slide:b});a.find(".slider").slider(f)}),this.dataset.metadata_column_names,a},_render_chartDisplay:function(){var a=this.$el.find(".tab-pane#chart-display");return this.plotView.setElement(a),this.plotView.render(),a},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart"},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.updateConfigWithDataSettings(),this.updateConfigWithChartSettings(),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.plotView.fetchData()},updateConfigWithDataSettings:function(){var a=this.$el.find("#data-control"),b={xColumn:Number(a.find('[name="xColumn"]').val()),yColumn:Number(a.find('[name="yColumn"]').val())};return a.find("#include-id-checkbox").prop("checked")&&(b.idColumn=a.find('[name="idColumn"]').val()),_.extend(this.plotView.config,b)},updateConfigWithChartSettings:function(){var a=this.plotView,b=this.$el.find("#chart-control");return["datapointSize","width","height"].forEach(function(c){a.config[c]=b.find('.numeric-slider-input[data-config-key="'+c+'"]').find(".slider").slider("value")}),a.config.x.label=b.find('input[name="X-axis-label"]').val(),a.config.y.label=b.find('input[name="Y-axis-label"]').val(),a.config},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:Templates.editor,dataControl:Templates.datacontrol,chartControl:Templates.chartcontrol};var ScatterplotView=Backbone.View.extend({defaults:{metadata:{dataLines:void 0},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},initialize:function(a){this.config=_.extend(_.clone(this.defaults),a.config||{}),this.dataset=a.dataset},updateConfig:function(a){this.config=this.config||{},_.extend(this.config,a)},fetchData:function(){this.showLoadingIndicator("getting data");var a=this;return xhr=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:this.config.pagination.perPage,offset:this.config.pagination.currPage*this.config.pagination.perPage}),xhr.done(function(b){a.renderData(b.data)}),xhr.fail(function(a,b,c){alert("Error loading data:\n"+a.responseText),console.error(a,b,c)}),xhr.always(function(){a.hideLoadingIndicator()}),xhr},render:function(a){return this.$el.addClass("scatterplot-display").html(['<div class="controls clear"></div>','<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message"></span>',"</div>","<svg/>",'<div class="stats-display"></div>'].join("")),this.$el.children().hide(),a&&this.renderData(a),this},showLoadingIndicator:function(a,b){a=a||"",b=b||"fast";var c=this.$el.find(".loading-indicator");a&&c.find(".loading-indicator-message").text(a),c.is(":visible")||(this.toggleStats(!1),c.css({left:this.config.width/2,top:this.config.height/2}).show())},hideLoadingIndicator:function(a){a=a||"fast",this.$el.find(".loading-indicator").hide()},renderData:function(a){this.$el.find(".controls").empty().append(this.renderControls(a)).show(),this.renderPlot(a),this.getStats(a)},renderControls:function(a){var b=this,c=$('<div class="left"></div>'),d=$('<div class="right"></div>');return c.append([this.renderPrevNext(a),this.renderPagination(a)]),d.append([this.renderLineInfo(a),$("<button>Stats</button>").addClass("stats-toggle-btn").click(function(){b.toggleStats()}),$("<button>Redraw</button>").addClass("rerender-btn").click(function(){b.renderPlot(a)})]),[c,d]},renderLineInfo:function(a){var b=this.dataset.metadata_data_lines||"an unknown number of",c=this.config.pagination.currPage*this.config.pagination.perPage,d=c+a.length;return $("<p/>").addClass("scatterplot-data-info").text(["Displaying lines",c+1,"to",d,"of",b,"lines"].join(" "))},renderPrevNext:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;var c=this,d=this.dataset.metadata_data_lines,e=d?Math.ceil(d/this.config.pagination.perPage):void 0,f=b("Prev").click(function(){c.config.pagination.currPage>0&&(c.config.pagination.currPage-=1,c.fetchData())}),g=b("Next").click(function(){(!e||c.config.pagination.currPage<e-1)&&(c.config.pagination.currPage+=1,c.fetchData())}),h=$("<ul/>").addClass("pagination data-prev-next").append([f,g]);return 0===c.config.pagination.currPage&&f.addClass("disabled"),e&&c.config.pagination.currPage===e-1&&g.addClass("disabled"),h},renderPagination:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}function c(){d.config.pagination.currPage=$(this).data("page"),d.fetchData()}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;for(var d=this,e=this.dataset.metadata_data_lines,f=e?Math.ceil(e/this.config.pagination.perPage):void 0,g=$("<ul/>").addClass("pagination data-pages"),h=0;f>h;h+=1){var i=b(h+1).attr("data-page",h).click(c);h===this.config.pagination.currPage&&i.addClass("active"),g.append(i)}return g},renderPlot:function(a){this.toggleStats(!1);var b=this.$el.find("svg");b.off().empty().show(),scatterplot(b.get(0),this.config,a)},getStats:function(a){var b=this;meanWorker=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js"),meanWorker.postMessage({data:a,keys:[this.config.xColumn,this.config.yColumn]}),meanWorker.onerror=function(){meanWorker.terminate()},meanWorker.onmessage=function(a){b.renderStats(a.data)}},renderStats:function(a){var b=this.$el.find(".stats-display"),c=this.config.x.label,d=this.config.y.label,e=$("<table/>").addClass("table").append(["<thead><th></th><th>",c,"</th><th>",d,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));b.empty().append(e)},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()"}}); \ 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(){q.redraw(),e(),s=d(),$(".chart-info-box").remove(),$(o.node()).trigger("zoom.scatterplot",[])}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',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,10]),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=4;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",b.height).attr("r",0);t.transition().duration(b.animDuration).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",b.datapointSize),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 </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"})),console.log(this+".initialize, attributes:",a),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,console.log("dataset:",this.dataset),this.plotView=new ScatterplotView({dataset:a.dataset,model:this.model})},render:function(){return this.$el.append(ScatterplotConfigEditor.templates.mainLayout({})),this.$el.find("#data-control").append(this._render_dataControl()),this._render_chartControls(this.$el.find("#chart-control")),this._render_chartDisplay(),this.$el.find("[title]").tooltip(),this},_render_dataControl:function(){var a=this.dataset,b=_.map(a.metadata_column_types,function(b,c){var d={index:c,type:b,name:"column "+(c+1)};return a.metadata_column_names&&a.metadata_column_names[c]&&(d.name=a.metadata_column_names[c]),d}),c=_.filter(b,function(a){return"int"===a.type||"float"===a.type});2>c&&(c=b);var d=this.$el.find(".tab-pane#data-control");return d.html(ScatterplotConfigEditor.templates.dataControl({allColumns:b,numericColumns:c})),d.find('[name="xColumn"]').val(this.plotView.config.xColumn||c[0].index),d.find('[name="yColumn"]').val(this.plotView.config.yColumn||c[1].index),void 0!==this.plotView.config.idColumn&&(d.find("#include-id-checkbox").prop("checked",!0).trigger("change"),d.find('select[name="idColumn"]').val(this.plotView.config.idColumn)),d},_render_chartControls:function(a){function b(){var a=$(this);a.siblings(".slider-output").text(a.slider("value"))}a.html(ScatterplotConfigEditor.templates.chartControl(this.plotView.config));var c=this,d={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};return a.find(".numeric-slider-input").each(function(){var a=$(this),e=a.attr("data-config-key"),f=_.extend(d[e],{value:c.plotView.config[e],change:b,slide:b});a.find(".slider").slider(f)}),this.dataset.metadata_column_names,a},_render_chartDisplay:function(){var a=this.$el.find(".tab-pane#chart-display");return this.plotView.setElement(a),this.plotView.render(),a},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart"},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.updateConfigWithDataSettings(),this.updateConfigWithChartSettings(),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.plotView.fetchData()},updateConfigWithDataSettings:function(){var a=this.$el.find("#data-control"),b={xColumn:Number(a.find('[name="xColumn"]').val()),yColumn:Number(a.find('[name="yColumn"]').val())};return a.find("#include-id-checkbox").prop("checked")&&(b.idColumn=a.find('[name="idColumn"]').val()),_.extend(this.plotView.config,b)},updateConfigWithChartSettings:function(){var a=this.plotView,b=this.$el.find("#chart-control");return["datapointSize","width","height"].forEach(function(c){a.config[c]=b.find('.numeric-slider-input[data-config-key="'+c+'"]').find(".slider").slider("value")}),a.config.x.label=b.find('input[name="X-axis-label"]').val(),a.config.y.label=b.find('input[name="Y-axis-label"]').val(),a.config},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotView=Backbone.View.extend({defaults:{metadata:{dataLines:void 0},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},initialize:function(a){this.config=_.extend(_.clone(this.defaults),a.config||{}),this.dataset=a.dataset},updateConfig:function(a){this.config=this.config||{},_.extend(this.config,a)},fetchData:function(){this.showLoadingIndicator("getting data");var a=this;return xhr=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:this.config.pagination.perPage,offset:this.config.pagination.currPage*this.config.pagination.perPage}),xhr.done(function(b){a.renderData(b.data)}),xhr.fail(function(a,b,c){alert("Error loading data:\n"+a.responseText),console.error(a,b,c)}),xhr.always(function(){a.hideLoadingIndicator()}),xhr},render:function(a){return this.$el.addClass("scatterplot-display").html(['<div class="controls clear"></div>','<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message"></span>',"</div>","<svg/>",'<div class="stats-display"></div>'].join("")),this.$el.children().hide(),a&&this.renderData(a),this},showLoadingIndicator:function(a,b){a=a||"",b=b||"fast";var c=this.$el.find(".loading-indicator");a&&c.find(".loading-indicator-message").text(a),c.is(":visible")||(this.toggleStats(!1),c.css({left:this.config.width/2,top:this.config.height/2}).show())},hideLoadingIndicator:function(a){a=a||"fast",this.$el.find(".loading-indicator").hide()},renderData:function(a){this.$el.find(".controls").empty().append(this.renderControls(a)).show(),this.renderPlot(a),this.getStats(a)},renderControls:function(a){var b=this,c=$('<div class="left"></div>'),d=$('<div class="right"></div>');return c.append([this.renderPrevNext(a),this.renderPagination(a)]),d.append([this.renderLineInfo(a),$("<button>Stats</button>").addClass("stats-toggle-btn").click(function(){b.toggleStats()}),$("<button>Redraw</button>").addClass("rerender-btn").click(function(){b.renderPlot(a)})]),[c,d]},renderLineInfo:function(a){var b=this.dataset.metadata_data_lines||"an unknown number of",c=this.config.pagination.currPage*this.config.pagination.perPage,d=c+a.length;return $("<p/>").addClass("scatterplot-data-info").text(["Displaying lines",c+1,"to",d,"of",b,"lines"].join(" "))},renderPrevNext:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;var c=this,d=this.dataset.metadata_data_lines,e=d?Math.ceil(d/this.config.pagination.perPage):void 0,f=b("Prev").click(function(){c.config.pagination.currPage>0&&(c.config.pagination.currPage-=1,c.fetchData())}),g=b("Next").click(function(){(!e||c.config.pagination.currPage<e-1)&&(c.config.pagination.currPage+=1,c.fetchData())}),h=$("<ul/>").addClass("pagination data-prev-next").append([f,g]);return 0===c.config.pagination.currPage&&f.addClass("disabled"),e&&c.config.pagination.currPage===e-1&&g.addClass("disabled"),h},renderPagination:function(a){function b(a){return $(['<li><a href="javascript:void(0);">',a,"</a></li>"].join(""))}function c(){d.config.pagination.currPage=$(this).data("page"),d.fetchData()}if(!a||0===this.config.pagination.currPage&&a.length<this.config.pagination.perPage)return null;for(var d=this,e=this.dataset.metadata_data_lines,f=e?Math.ceil(e/this.config.pagination.perPage):void 0,g=$("<ul/>").addClass("pagination data-pages"),h=0;f>h;h+=1){var i=b(h+1).attr("data-page",h).click(c);h===this.config.pagination.currPage&&i.addClass("active"),g.append(i)}return g},renderPlot:function(a){this.toggleStats(!1);var b=this.$el.find("svg");b.off().empty().show(),scatterplot(b.get(0),this.config,a)},getStats:function(a){var b=this;meanWorker=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js"),meanWorker.postMessage({data:a,keys:[this.config.xColumn,this.config.yColumn]}),meanWorker.onerror=function(){meanWorker.terminate()},meanWorker.onmessage=function(a){b.renderStats(a.data)}},renderStats:function(a){var b=this.$el.find(".stats-display"),c=this.config.x.label,d=this.config.y.label,e=$("<table/>").addClass("table").append(["<thead><th></th><th>",c,"</th><th>",d,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));b.empty().append(e)},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:{metadata:{dataLines:void 0},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}}}); \ No newline at end of file diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -1,3 +1,24 @@ +<% + hda_dict = trans.security.encode_dict_ids( hda.to_dict() ) + + config = query_args + title = "Scatterplot of '" + hda.name + "'" + info = hda.info + + visualization = context.get( 'visualization' ) + if visualization is not None: + config = visualization.latest_revision.config + config.update( query_args ) + title = visualization.title + info = config.get( 'description', info ) + + config[ 'type' ] = 'scatterplot' + + # optionally bootstrap data from dprov + ##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) ) +%> +## ---------------------------------------------------------------------------- + <!DOCTYPE HTML><html><head> @@ -13,14 +34,15 @@ ## ---------------------------------------------------------------------------- <script type="text/javascript" src="/static/scripts/libs/jquery/jquery.js"></script><script type="text/javascript" src="/static/scripts/libs/jquery/jquery.migrate.js"></script> +<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script> +<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script><script type="text/javascript" src="/static/scripts/libs/underscore.js"></script><script type="text/javascript" src="/static/scripts/libs/backbone/backbone.js"></script><script type="text/javascript" src="/static/scripts/libs/handlebars.runtime.js"></script><script type="text/javascript" src="/static/scripts/libs/d3.js"></script> -<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script> -<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script> -<script type="text/javascript" src="/static/scripts/utils/LazyDataLoader.js"></script> + <script type="text/javascript" src="/static/scripts/mvc/base-mvc.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></head> @@ -30,25 +52,20 @@ %if not embedded: ## dataset info: only show if on own page <div class="chart-header"> - <h2>Scatterplot of '${hda.name}'</h2> - <p>${hda.info}</p> + <h2>${title}</h2> + <p>${info}</p></div><div class="scatterplot-editor"></div><script type="text/javascript"> $(function(){ - <% - # optionally, bootstrap data from dprov - data = None - ##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) ) - %> - var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )}; - - var editor = new ScatterplotConfigEditor({ - el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hda.id ), - config : ${h.to_json_string( query_args )}, - dataset : ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )} - }).render(); + var model = new ScatterplotModel( ${h.to_json_string( config )} ), + hdaJson = ${h.to_json_string( hda_dict )}, + editor = new ScatterplotConfigEditor({ + el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hdaJson.id ), + model : model, + dataset : hdaJson + }).render(); window.editor = editor; // uncomment to auto render for development //$( '.render-button:visible' ).click(); diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 lib/galaxy/app.py --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -87,9 +87,9 @@ load_genome_index_tools( self.toolbox ) # visualizations registry: associates resources with visualizations, controls how to render self.visualizations_registry = None - if self.config.visualizations_plugins_directory: + if self.config.visualization_plugins_directory: self.visualizations_registry = VisualizationsRegistry( self, - directories_setting=self.config.visualizations_plugins_directory, + directories_setting=self.config.visualization_plugins_directory, template_cache_dir=self.config.template_cache ) # Load security policy. self.security_agent = self.model.security_agent diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -343,7 +343,7 @@ self.fluent_host = kwargs.get( 'fluent_host', 'localhost' ) self.fluent_port = int( kwargs.get( 'fluent_port', 24224 ) ) # visualization plugin framework - self.visualizations_plugins_directory = kwargs.get( 'visualizations_plugins_directory', None ) + self.visualization_plugins_directory = kwargs.get( 'visualization_plugins_directory', None ) @property def sentry_dsn_public( self ): diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 lib/galaxy/visualization/registry.py --- a/lib/galaxy/visualization/registry.py +++ b/lib/galaxy/visualization/registry.py @@ -38,6 +38,61 @@ user_pref for ordering/ex/inclusion of particular visualizations """ +# ------------------------------------------------------------------- misc +#TODO: move to utils? +def getattr_recursive( item, attr_key, *args ): + """ + Allows dot member notation in attribute name when getting an item's attribute. + + NOTE: also searches dictionaries + """ + #print '\n\t getattr_recursive:', item, attr_key + using_default = len( args ) >= 1 + default = args[0] if using_default else None + #print '\t defaults:', using_default, default + + for attr_key in attr_key.split( '.' ): + try: + #print '\t\t attr:', attr_key, 'item:', item + if isinstance( item, dict ): + item = item.__getitem__( attr_key ) + else: + item = getattr( item, attr_key ) + + except ( KeyError, AttributeError ), err: + #print '\t\t error:', err + if using_default: + #print '\t\t\t default:', default + return default + raise + + return item + +def hasattr_recursive( item, attr_key ): + """ + Allows dot member notation in attribute name when getting an item's attribute. + + NOTE: also searches dictionaries + """ + if '.' in attr_key: + attr_key, last_key = attr_key.rsplit( '.', 1 ) + item = getattr_recursive( item, attr_key, None ) + if item is None: + return False + attr_key = last_key + + try: + #print '\t\t attr:', attr_key, 'item:', item + if isinstance( item, dict ): + return item.__contains__( attr_key ) + else: + return hasattr( item, attr_key ) + + except ( KeyError, AttributeError ), err: + return False + + return True + # ------------------------------------------------------------------- the registry class VisualizationsRegistry( pluginframework.PageServingPluginManager ): """ @@ -141,21 +196,26 @@ `visualization_name` if it's applicable to `target_object` or `None` if it's not. """ + #log.debug( 'VisReg.get_visualization: %s, %s', visualization_name, target_object ) visualization = self.plugins.get( visualization_name, None ) if not visualization: return None data_sources = visualization.config[ 'data_sources' ] for data_source in data_sources: + #log.debug( 'data_source: %s', data_source ) # currently a model class is required model_class = data_source[ 'model_class' ] + #log.debug( '\t model_class: %s', model_class ) if not isinstance( target_object, model_class ): continue + #log.debug( '\t passed model_class' ) # tests are optional - default is the above class test tests = data_source[ 'tests' ] if tests and not self.is_object_applicable( trans, target_object, tests ): continue + #log.debug( '\t passed tests' ) param_data = data_source[ 'to_params' ] url = self.get_visualization_url( trans, target_object, visualization_name, param_data ) @@ -199,7 +259,7 @@ #NOTE: tests are OR'd, if any test passes - the visualization can be applied if test_fn( target_object, test_result ): - #log.debug( 'test passed' ) + #log.debug( '\t test passed' ) return True return False @@ -212,6 +272,8 @@ #precondition: the target_object should be usable by the visualization (accrd. to data_sources) # convert params using vis.data_source.to_params params = self.get_url_params( trans, target_object, param_data ) + #for param in params: + # print param # we want existing visualizations to work as normal but still be part of the registry (without mod'ing) # so generate their urls differently @@ -238,13 +300,20 @@ # one or the other is needed # assign takes precedence (goes last, overwrites)? #NOTE this is only one level - if target_attr and hasattr( target_object, target_attr ): - params[ to_param_name ] = getattr( target_object, target_attr ) + + #print 'target_object:', target_object + #print 'target_attr:', target_attr + + if target_attr and hasattr_recursive( target_object, target_attr ): + params[ to_param_name ] = getattr_recursive( target_object, target_attr ) + #print 'params[ %s ]:' %( to_param_name ), params[ to_param_name ] + if assign: params[ to_param_name ] = assign #NOTE!: don't expose raw ids: encode id, _id if params: +#TODO: double encodes if from config params = trans.security.encode_dict_ids( params ) return params @@ -407,6 +476,7 @@ # these are the allowed classes to associate visualizations with (as strings) # any model_class element not in this list will throw a parsing ParsingExcepion ALLOWED_MODEL_CLASSES = [ + 'Visualization', 'HistoryDatasetAssociation', 'LibraryDatasetDatasetAssociation' ] @@ -683,6 +753,9 @@ If param is required and not present, raises a `KeyError`. """ + # first parse any params from any visualizations that were passed + query_params = self.get_params_from_visualization_param( trans, controller, param_config_dict, query_params ) + # parse the modifiers first since they modify the params coming next #TODO: this is all really for hda_ldda - which we could replace with model polymorphism params_that_modify_other_params = self.parse_parameter_modifiers( @@ -761,6 +834,44 @@ # (and adding this code to the xml parser) return self.parse_parameter( trans, param_config, default ) + def get_params_from_visualization_param( self, trans, controller, param_config_dict, query_params ): + log.debug( 'parse_visualization_params: %s', param_config_dict ) + log.debug( ' : %s', query_params ) + + # first, find the visualization in the parameters if any + visualization = None + #precondition: assume one visualization + for param_name, param_config in param_config_dict.items(): + if param_config.get( 'type' ) == 'visualization': + query_val = query_params.get( param_name ) + if query_val is None: + continue + + log.debug( 'found visualization param: %s, %s', param_name, query_val ) + visualization = self.parse_parameter( trans, controller, param_config, query_val ) + if visualization: + break + + # if no vis is found, can't get any new params from it: return the original query_params + if not visualization: + log.debug( 'visualization not found' ) + return query_params + log.debug( 'found visualization: %s', visualization ) + + # next, attempt to copy any params from the visualizations config + visualization_config = visualization.latest_revision.config + log.debug( '\t config: %s', visualization_config ) + params_from_visualization = {} + for param_name, param_config in param_config_dict.items(): + if param_name in visualization_config: + params_from_visualization[ param_name ] = visualization_config[ param_name ] + log.debug( 'params_from_visualization: %s', params_from_visualization ) + + # layer the query_params over the params from the visualization, returning the combined + params_from_visualization.update( query_params ) + return params_from_visualization + +#TODO: make parse_visualization separate def parse_parameter( self, trans, controller, expected_param_data, query_param, recurse=True, param_modifiers=None ): """ @@ -808,6 +919,7 @@ #TODO: subclass here? elif param_type == 'visualization': encoded_visualization_id = query_param + log.debug( 'visualization param, id : %s', encoded_visualization_id ) #TODO:?? some fallback if there's no get_X in controller that's passed? parsed_param = controller.get_visualization( trans, encoded_visualization_id, check_ownership=False, check_accessible=True ) diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/mvc/dataset/hda-edit.js --- a/static/scripts/mvc/dataset/hda-edit.js +++ b/static/scripts/mvc/dataset/hda-edit.js @@ -248,7 +248,6 @@ classes : 'dataset-visualize-btn', faIcon : 'fa-bar-chart-o' }); - $icon.addClass( 'visualize-icon' ); // needed? // No need for popup menu because there's a single visualization. if( _.keys( visualizations ).length === 1 ) { @@ -259,9 +258,21 @@ } else { var popup_menu_options = []; _.each( visualizations, function( linkData ) { + linkData.func = function(){ + if( Galaxy.frame.active ){ + Galaxy.frame.add({ + title : "Visualization", + type : "url", + content : linkData.href + }); + return false; + } + return true; + }; popup_menu_options.push( linkData ); + return false; }); - var popup = new PopupMenu( $icon, popup_menu_options ); + PopupMenu.create( $icon, popup_menu_options ); } return $icon; }, diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/mvc/ui.js --- a/static/scripts/mvc/ui.js +++ b/static/scripts/mvc/ui.js @@ -359,6 +359,10 @@ return 'PopupMenu'; } }); +/** shortcut to new for when you don't need to preserve the ref */ +PopupMenu.create = function _create( $button, options ){ + return new PopupMenu( $button, options ); +}; // ----------------------------------------------------------------------------- // the following class functions are bridges from the original make_popupmenu and make_popup_menus diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/mvc/visualization/scatterplotControlForm.js --- /dev/null +++ b/static/scripts/mvc/visualization/scatterplotControlForm.js @@ -0,0 +1,631 @@ +/* ============================================================================= +todo: + I'd like to move the svg creation out of the splot constr. to: + allow adding splots to an existing canvas + allow mult. splots sharing a canvas + + + outside this: + BUG: setting width, height in plot controls doesn't re-interpolate data locations!! + BUG?: get metadata_column_names (from datatype if necessary) + BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations' + + wire label setters, anim setter + + TwoVarScatterplot: + ??: maybe better to do this with a canvas... + save as visualization + to seperate file? + remove underscore dependencies + add interface to change values (seperate)? + download svg -> base64 encode + incorporate glyphs, glyph state renderers + + ScatterplotSettingsForm: + some css bug that lowers the width of settings form when plot-controls tab is open + causes chart to shift + what can be abstracted/reused for other graphs? + avoid direct manipulation of this.plot + allow option to put plot into seperate tab of interface (for small multiples) + + provide callback in view to load data incrementally - for large sets + paginate + handle rerender + use endpoint (here and on the server (fileptr)) + fetch (new?) data + handle rerender + use d3.TSV? + render warning on long data (> maxDataPoints) + adjust endpoint + + selectable list of preset column comparisons (rnaseq etc.) + how to know what sort of Tabular the data is? + smarter about headers + validate columns selection (here or server) + + set stats column names by selected columns + move chart into tabbed area... + + Scatterplot.mako: + multiple plots on one page (small multiples) + ?? ensure svg styles thru d3 or css? + d3: configable (easily) + css: standard - better maintenance + ? override at config + +============================================================================= */ +/** + * Scatterplot control UI as a backbone view + * handles: + * getting the desired data + * configuring the plot display + * showing (general) statistics + * + * initialize attributes REQUIRES a dataset and an apiDatasetsURL + */ +var ScatterplotControlForm = Backbone.View.extend( LoggableMixin ).extend({ + //logger : console, + className : 'scatterplot-control-form', + + //NOTE: should include time needed to render + dataLoadDelay : 4000, + dataLoadSize : 5000, + + loadingIndicatorImage : 'loading_small_white_bg.gif', + fetchMsg : 'Fetching data...', + renderMsg : 'Rendering...', + + initialize : function( attributes ){ + this.log( this + '.initialize, attributes:', attributes ); + + this.dataset = null; + this.chartConfig = null; + this.chart = null; + this.loader = null; + + // set up refs to the four tab areas + this.$dataControl = null; + this.$chartControl = null; + this.$statsDisplay = null; + this.$chartDisplay = null; + + this.dataFetch = null; + + this.initializeFromAttributes( attributes ); + this.initializeChart( attributes ); + this.initializeDataLoader( attributes ); + }, + + initializeFromAttributes : function( attributes ){ + // required settings: ensure certain vars we need are passed in attributes + if( !attributes || !attributes.dataset ){ + throw( "ScatterplotView requires a dataset" ); + } else { + this.dataset = attributes.dataset; + } + if( jQuery.type( this.dataset.metadata_column_types ) === 'string' ){ + this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' ); + } + this.log( '\t dataset:', this.dataset ); + + // attempt to get possible headers from the data's first line + if( this.dataset.comment_lines && this.dataset.comment_lines.length ){ + //TODO:?? + var firstLine = this.dataset.comment_lines[0], + possibleHeaders = firstLine.split( '\t' ); + if( possibleHeaders.length === this.dataset.metadata_column_types.length ){ + this.possibleHeaders = possibleHeaders; + } + } + + // passed from mako helper + //TODO: integrate to galaxyPaths + //TODO: ?? seems like data loader section would be better + if( !attributes.apiDatasetsURL ){ + throw( "ScatterplotView requires a apiDatasetsURL" ); + } else { + this.dataURL = attributes.apiDatasetsURL + '/' + this.dataset.id + '?'; + } + this.log( '\t dataURL:', this.dataURL ); + }, + + initializeChart : function( attributes ){ + // set up the basic chart infrastructure and config (if any) + this.chartConfig = attributes.chartConfig || {}; + //if( this.logger ){ this.chartConfig.debugging = true; } + this.log( '\t initial chartConfig:', this.chartConfig ); + + this.chart = new TwoVarScatterplot( this.chartConfig ); + //TODO: remove 2nd ref, use this.chart.config + this.chartConfig = this.chart.config; + }, + + initializeDataLoader : function( attributes ){ + // set up data loader + var view = this; + this.loader = new LazyDataLoader({ + //logger : ( this.logger )?( this.logger ):( null ), + // we'll generate this when columns are chosen + url : null, + start : attributes.start || 0, + //NOTE: metadata_data_lines can be null (so we won't know the total) + total : attributes.total || this.dataset.metadata_data_lines, + delay : this.dataLoadDelay, + size : this.dataLoadSize, + + buildUrl : function( start, size ){ + // currently VERY SPECIFIC to using data_providers.py start_val, max_vals params + return this.url + '&' + jQuery.param({ + start_val: start, + max_vals: size + }); + } + }); + $( this.loader ).bind( 'error', function( event, status, error ){ + view.log( 'ERROR:', status, error ); + alert( 'ERROR fetching data:\n' + status + '\n' + error ); + view.hideLoadingIndicator(); + }); + }, + + // ------------------------------------------------------------------------- CONTROLS RENDERING + render : function(){ + this.log( this + '.render' ); + + // render the tab controls, areas and loading indicator + this.$el.append( ScatterplotControlForm.templates.mainLayout({ + loadingIndicatorImagePath : galaxy_config.root + 'static/images/' + this.loadingIndicatorImage, + message : '' + })); + + // render the tab content + this.$dataControl = this._render_dataControl(); + this.$chartControl = this._render_chartControl(); + this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' ); + this.$chartDisplay = this._render_chartDisplay(); + + // auto render if given both x, y column choices in query for page + //TODO:?? add autoRender=1 to query maybe? + if( this.chartConfig.xColumn && this.chartConfig.yColumn ){ + this.renderChart(); + } + + // set up behaviours + this.$el.find( '[title]' ).tooltip(); + + // uncomment any of the following to have that tab show on initial load (for testing) + //this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + return this; + }, + + _render_dataControl : function(){ + // controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) + var view = this, + allColumns = [], + numericColumns = [], + usePossibleHeaders = ( this.possibleHeaders && this.$dataControl )? + ( this.$dataControl.find( '#first-line-header-checkbox' ).is( ':checked' ) ):( false ); + + // gather column indeces (from metadata_column_types) and names (from metadata_columnnames) + _.each( this.dataset.metadata_column_types, function( type, index ){ + // use a 1 based index in names/values within the form (will be dec. when parsed out) + var oneBasedIndex = index + 1, + // default name is 'column <index>'... + name = 'column ' + oneBasedIndex; + + // ...but label with the name if available... + if( view.dataset.metadata_column_names ){ + name = view.dataset.metadata_column_names[ index ]; + + // ...or, use the first line as headers if the user wants + } else if( usePossibleHeaders ){ + name = view.possibleHeaders[ index ]; + } + + // cache all columns here + allColumns.push({ index: oneBasedIndex, name: name }); + + // filter numeric columns to their own list + if( type === 'int' || type === 'float' ){ + numericColumns.push({ index: oneBasedIndex, name: name }); + } + }); + //TODO: other vals: max_vals, start_val, pagination (chart-settings) + + // render the html + var $dataControl = this.$el.find( '.tab-pane#data-control' ); + $dataControl.html( ScatterplotControlForm.templates.dataControl({ + allColumns : allColumns, + numericColumns : numericColumns, + possibleHeaders : ( this.possibleHeaders )?( this.possibleHeaders.join( ', ' ) ):( '' ), + usePossibleHeaders : usePossibleHeaders + })); + + if( !this.dataset.metadata_column_names && this.possibleHeaders ){ + $dataControl.find( '#first-line-header' ).show(); + } + + // preset to column selectors if they were passed in the config in the query string + $dataControl.find( '#X-select' ).val( this.chartConfig.xColumn ); + $dataControl.find( '#Y-select' ).val( this.chartConfig.yColumn ); + if( this.chartConfig.idColumn !== undefined ){ + $dataControl.find( '#include-id-checkbox' ) + .attr( 'checked', true ).trigger( 'change' ); + $dataControl.find( '#ID-select' ).val( this.chartConfig.idColumn ); + } + + return $dataControl; + }, + + _render_chartControl : function(){ + // tab content to control how the chart is rendered (data glyph size, chart size, etc.) + var view = this, + $chartControl = this.$el.find( '.tab-pane#chart-control' ), + // limits for controls (by control/chartConfig id) + //TODO: move into TwoVarScatterplot + controlRanges = { + 'datapointSize' : { min: 2, max: 10, step: 1 }, + 'width' : { min: 200, max: 800, step: 20 }, + 'height' : { min: 200, max: 800, step: 20 } + }; + + // render the html + $chartControl.append( ScatterplotControlForm.templates.chartControl( this.chartConfig ) ); + + // set up behaviours, js on sliders + $chartControl.find( '.numeric-slider-input' ).each( function(){ + var $this = $( this ), + $output = $this.find( '.slider-output' ), + $slider = $this.find( '.slider' ), + id = $this.attr( 'id' ); + //chartControl.log( 'slider set up', 'this:', $this, 'slider:', $slider, 'id', id ); + + // what to do when the slider changes: update display and update chartConfig + //TODO: move out of loop + function onSliderChange(){ + var $this = $( this ), + newValue = $this.slider( 'value' ); + //chartControl.log( 'slider change', 'this:', $this, 'output:', $output, 'value', newValue ); + $output.text( newValue ); + //chartControl.chartConfig[ id ] = newValue; + } + + $slider.slider( _.extend( controlRanges[ id ], { + value : view.chartConfig[ id ], + change : onSliderChange, + slide : onSliderChange + })); + }); + + return $chartControl; + }, + + _render_chartDisplay : function(){ + // render the tab content where the chart is displayed (but not the chart itself) + var $chartDisplay = this.$el.find( '.tab-pane#chart-display' ); + $chartDisplay.append( ScatterplotControlForm.templates.chartDisplay( this.chartConfig ) ); + return $chartDisplay; + }, + + // ------------------------------------------------------------------------- EVENTS + events : { + 'change #include-id-checkbox' : 'toggleThirdColumnSelector', + 'change #first-line-header-checkbox' : 'rerenderDataControl', + 'click #data-control #render-button' : 'renderChart', + 'click #chart-control #render-button' : 'changeChartSettings' + }, + + toggleThirdColumnSelector : function(){ + // show/hide the id selector on the data settings panel + this.$el.find( 'select[name="ID"]' ).parent().toggle(); + }, + + rerenderDataControl : function(){ + this.$dataControl = this._render_dataControl(); + }, + + showLoadingIndicator : function( message, callback ){ + // display the loading indicator over the tab panels if hidden, update message (if passed) + message = message || ''; + var indicator = this.$el.find( 'div#loading-indicator' ); + messageBox = indicator.find( '.loading-message' ); + + if( indicator.is( ':visible' ) ){ + if( message ){ + messageBox.fadeOut( 'fast', function(){ + messageBox.text( message ); + messageBox.fadeIn( 'fast', callback ); + }); + } else { + callback(); + } + + } else { + if( message ){ messageBox.text( message ); } + indicator.fadeIn( 'fast', callback ); + } + }, + + hideLoadingIndicator : function( callback ){ + this.$el.find( 'div#loading-indicator' ).fadeOut( 'fast', callback ); + }, + + // ------------------------------------------------------------------------- CHART/STATS RENDERING + renderChart : function(){ + // fetch the data, (re-)render the chart + this.log( this + '.renderChart' ); + + //TODO: separate data fetch + + // this is a complete re-render, so clear the prev. data + this.data = null; + this.meta = null; + + // update the chartConfig (here and chart) using chart settings + //TODO: separate and improve (used in changeChartSettings too) + _.extend( this.chartConfig, this.getChartSettings() ); + this.log( '\t chartConfig:', this.chartConfig ); + this.chart.updateConfig( this.chartConfig, false ); + + // build the url with the current data settings + this.loader.url = this.dataURL + '&' + jQuery.param( this.getDataSettings() ); + this.log( '\t loader: total lines:', this.loader.total, ' url:', this.loader.url ); + + // bind the new data event to: aggregate data, update the chart and stats with new data + var view = this; + $( this.loader ).bind( 'loaded.new', function( event, response ){ + view.log( view + ' loaded.new', response ); + + // aggregate data and meta + view.postProcessDataFetchResponse( response ); + view.log( '\t postprocessed data:', view.data ); + view.log( '\t postprocessed meta:', view.meta ); + + // update the chart and stats + view.showLoadingIndicator( view.renderMsg, function(){ + view.chart.render( view.data, view.meta ); + view.renderStats( view.data, view.meta ); + view.hideLoadingIndicator(); + }); + }); + // when all data loaded - unbind (or we'll start doubling event handlers) + $( this.loader ).bind( 'complete', function( event, data ){ + view.log( view + ' complete', data ); + $( view.loader ).unbind(); + }); + + // begin loading the data, switch to the chart display tab + view.showLoadingIndicator( view.fetchMsg, function(){ + view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + view.loader.load(); + }); + }, + + renderStats : function(){ + this.log( this + '.renderStats' ); + // render the stats table in the stats panel + //TODO: there's a better way + this.$statsDisplay.html( ScatterplotControlForm.templates.statsDisplay({ + stats: [ + { name: 'Count', xval: this.meta[0].count, yval: this.meta[1].count }, + { name: 'Min', xval: this.meta[0].min, yval: this.meta[1].min }, + { name: 'Max', xval: this.meta[0].max, yval: this.meta[1].max }, + { name: 'Sum', xval: this.meta[0].sum, yval: this.meta[1].sum }, + { name: 'Mean', xval: this.meta[0].mean, yval: this.meta[1].mean }, + { name: 'Median', xval: this.meta[0].median, yval: this.meta[1].median } + ] + })); + }, + + changeChartSettings : function(){ + // re-render the chart with new chart settings and OLD data + var view = this; + newChartSettings = this.getChartSettings(); + + // update the chart config from the chartSettings panel controls + _.extend( this.chartConfig, newChartSettings ); + this.log( 'this.chartConfig:', this.chartConfig ); + this.chart.updateConfig( this.chartConfig, false ); + + // if there's current data, call chart.render with it (no data fetch) + if( view.data && view.meta ){ + view.showLoadingIndicator( view.renderMsg, function(){ + view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + view.chart.render( view.data, view.meta ); + view.hideLoadingIndicator(); + }); + + // no current data, call renderChart instead (which will fetch data) + } else { + this.renderChart(); + } + }, + + // ------------------------------------------------------------------------- DATA AGGREGATION + postProcessDataFetchResponse : function( response ){ + // the loader only returns new data - it's up to this to munge the fetches together properly + //TODO: we're now storing data in two places: loader and here + // can't we reduce incoming data into loader.data[0]? are there concurrency problems? + this.postProcessData( response.data ); + this.postProcessMeta( response.meta ); + }, + + postProcessData : function( newData ){ + // stack the column data on top of each other into this.data + //this.log( this + '.postProcessData:', newData ); + var view = this; + + // if we already have data: aggregate + if( view.data ){ + _.each( newData, function( newColData, colIndex ){ + //view.log( colIndex + ' data:', newColData ); + //TODO??: time, space efficiency of this? + view.data[ colIndex ] = view.data[ colIndex ].concat( newColData ); + }); + + // otherwise: assign (first load) + } else { + view.data = newData; + } + }, + + postProcessMeta : function( newMeta ){ + // munge the meta data (stats) from the server fetches together + //pre: this.data must be preprocessed (needed for medians) + //this.log( this + '.postProcessMeta:', newMeta ); + var view = this, + colTypes = this.dataset.metadata_column_types; + + // if we already have meta: aggregate + if( view.meta ){ + _.each( newMeta, function( newColMeta, colIndex ){ + var colMeta = view.meta[ colIndex ], + colType = colTypes[ colIndex ]; + //view.log( '\t ' + colIndex + ' postprocessing meta:', newColMeta ); + //view.log( colIndex + ' old meta:', + // 'min:', colMeta.min, + // 'max:', colMeta.max, + // 'sum:', colMeta.sum, + // 'mean:', colMeta.mean, + // 'median:', colMeta.median + //); + + //!TODO: at what point are we getting int/float overflow on these?! + //??: need to be null safe? + colMeta.count += ( newColMeta.count )?( newColMeta.count ):( 0 ); + //view.log( colIndex, 'count:', colMeta.count ); + + if( ( colType === 'int' ) || ( colType === 'float' ) ){ + //view.log( colIndex + ' incoming meta:', + // 'min:', newColMeta.min, + // 'max:', newColMeta.max, + // 'sum:', newColMeta.sum, + // 'mean:', newColMeta.mean, + // 'median:', newColMeta.median + //); + + colMeta.min = Math.min( newColMeta.min, colMeta.min ); + colMeta.max = Math.max( newColMeta.max, colMeta.max ); + colMeta.sum = newColMeta.sum + colMeta.sum; + colMeta.mean = ( colMeta.count )?( colMeta.sum / colMeta.count ):( null ); + + // median's a pain bc of sorting (requires the data as well) + var sortedCol = view.data[ colIndex ].slice().sort(), + middleIndex = Math.floor( sortedCol.length / 2 ); + + if( sortedCol.length % 2 === 0 ){ + colMeta.median = ( ( sortedCol[ middleIndex ] + sortedCol[( middleIndex + 1 )] ) / 2 ); + + } else { + colMeta.median = sortedCol[ middleIndex ]; + } + + //view.log( colIndex + ' new meta:', + // 'min:', colMeta.min, + // 'max:', colMeta.max, + // 'sum:', colMeta.sum, + // 'mean:', colMeta.mean, + // 'median:', colMeta.median + //); + } + }); + + // otherwise: assign (first load) + } else { + view.meta = newMeta; + //view.log( '\t meta (first load):', view.meta ); + } + }, + + // ------------------------------------------------------------------------- GET DATA/CHART SETTINGS + getDataSettings : function(){ + // parse the column values for both indeces (for the data fetch) and names (for the chart) + var columnSelections = this.getColumnSelections(), + columns = []; + this.log( '\t columnSelections:', columnSelections ); + + //TODO: validate columns - minimally: we can assume either set by selectors or via a good query string + + // get column indices for params, include the desired ID column (if any) + //NOTE: these are presented in human-readable 1 base index (to match the data.peek) - adjust + columns = [ + columnSelections.X.colIndex - 1, + columnSelections.Y.colIndex - 1 + ]; + if( this.$dataControl.find( '#include-id-checkbox' ).attr( 'checked' ) ){ + columns.push( columnSelections.ID.colIndex - 1 ); + } + //TODO: other vals: max, start, page + + var params = { + data_type : 'raw_data', + provider : 'column_with_stats', + columns : '[' + columns + ']' + }; + this.log( '\t data settings (url params):', params ); + return params; + }, + + getColumnSelections : function(){ + // gets the current user-selected values for which columns to fetch from the data settings panel + // returns a map: { column-select name (eg. X) : { colIndex : column-selector val, + // colName : selected option text }, ... } + var selections = {}; + this.$dataControl.find( 'div.column-select select' ).each( function(){ + var $this = $( this ), + val = $this.val(); + selections[ $this.attr( 'name' ) ] = { + colIndex : val, + colName : $this.children( '[value="' + val + '"]' ).text() + }; + }); + return selections; + }, + + getChartSettings : function(){ + // gets the user-selected chartConfig from the chart settings panel + var settings = {}, + colSelections = this.getColumnSelections(); + //this.log( 'colSelections:', colSelections ); + + //TODO: simplify with keys and loop + settings.datapointSize = this.$chartControl.find( '#datapointSize.numeric-slider-input' ) + .find( '.slider' ).slider( 'value' ); + settings.width = this.$chartControl.find( '#width.numeric-slider-input' ) + .find( '.slider' ).slider( 'value' ); + settings.height = this.$chartControl.find( '#height.numeric-slider-input' ) + .find( '.slider' ).slider( 'value' ); + + // update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName + //TODO: a little confusing + var chartSettingsXLabel = this.$chartControl.find( 'input#X-axis-label' ).val(), + chartSettingsYLabel = this.$chartControl.find( 'input#Y-axis-label' ).val(); + settings.xLabel = ( chartSettingsXLabel === 'X' )? + ( colSelections.X.colName ):( chartSettingsXLabel ); + settings.yLabel = ( chartSettingsYLabel === 'Y' )? + ( colSelections.Y.colName ):( chartSettingsYLabel ); + + settings.animDuration = ( this.$chartControl.find( '#animate-chart' ).is( ':checked' ) )? + ( this.chart.defaults.animDuration ):( 0 ); + + this.log( '\t chartSettings:', settings ); + return settings; + }, + + toString : function(){ + return 'ScatterplotControlForm(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')'; + } +}); + +ScatterplotControlForm.templates = { + mainLayout : Handlebars.templates[ 'template-visualization-scatterplotControlForm' ], + dataControl : Handlebars.templates[ 'template-visualization-dataControl' ], + chartControl : Handlebars.templates[ 'template-visualization-chartControl' ], + statsDisplay : Handlebars.templates[ 'template-visualization-statsDisplay' ], + chartDisplay : Handlebars.templates[ 'template-visualization-chartDisplay' ] +}; + +//============================================================================== diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/mvc/visualization/visualization-model.js --- /dev/null +++ b/static/scripts/mvc/visualization/visualization-model.js @@ -0,0 +1,158 @@ +//define([ +//], function(){ + +//function saveVis( title, config ){ +// var xhr = jQuery.ajax( '/api/visualizations', { +// type : 'POST', +// data : { +// type : 'scatterplot', +// title : title, +// config : JSON.stringify( config ) +// } +// }); +// xhr.fail( function( xhr, status, message ){ +// console.debug( jQuery.makeArray( arguments ) ); +// console.error( 'Error saving visualization:', xhr.responseJSON.error ); +// }); +// return xhr.then( function( saveInfo ){ +// return saveInfo; +// }); +//} + +//============================================================================== +/** @class Model for a saved Galaxy visualization. + * + * @augments Backbone.Model + * @borrows LoggableMixin#logger as #logger + * @borrows LoggableMixin#log as #log + * @constructs + */ +var Visualization = Backbone.Model.extend( LoggableMixin ).extend( +/** @lends Visualization.prototype */{ + + ///** logger used to record this.log messages, commonly set to console */ + //// comment this out to suppress log output + //logger : console, + + /** default attributes for a model */ + defaults : { + //id : null, + //type : null, + //title : null, + //dbkey : null, + //user_id : null, + //slug : null, + //revisions : [], + //latest_revision : null + + //(this is unusual in that visualizations don't have configs, revisions do) + //config : {} + }, + + url : function(){ + return galaxy_config.root + 'api/visualizations'; + }, + + /** Set up the model, determine if accessible, bind listeners + * @see Backbone.Model#initialize + */ + initialize : function( data ){ + this.log( this + '.initialize', data, this.attributes ); + this._setUpListeners(); + }, + + /** set up any event listeners + */ + _setUpListeners : function(){ + }, + + // ........................................................................ config + setConfig: function( config ){ + var oldConfig = this.get( 'config' ); + // extend if already exists (and clone in order to trigger change) + if( _.isObject( oldConfig ) ){ + config = _.extend( _.clone( oldConfig ), config ); + } + this.set( 'config', config ); + return this; + }, + + // ........................................................................ common queries + // ........................................................................ ajax + // ........................................................................ misc + /** String representation */ + toString : function(){ + var idAndTitle = this.get( 'id' ) || ''; + if( this.get( 'title' ) ){ + idAndTitle += ':' + this.get( 'title' ); + } + return 'Visualization(' + idAndTitle + ')'; + } +}); + + +//============================================================================== +/** @class Backbone collection of visualization models + * + * @borrows LoggableMixin#logger as #logger + * @borrows LoggableMixin#log as #log + * @constructs + */ +var VisualizationCollection = Backbone.Collection.extend( LoggableMixin ).extend( +/** @lends VisualizationCollection.prototype */{ + model : Visualization, + + ///** logger used to record this.log messages, commonly set to console */ + //// comment this out to suppress log output + //logger : console, + + url : function(){ + return galaxy_config.root + 'api/visualizations'; + }, + + /** Set up. + * @see Backbone.Collection#initialize + */ + initialize : function( models, options ){ + options = options || {}; + //this._setUpListeners(); + }, + + //_setUpListeners : function(){ + //}, + + // ........................................................................ common queries + // ........................................................................ ajax + // ........................................................................ misc + set : function( models, options ){ + // arrrrrrrrrrrrrrrrrg... + // override to get a correct/smarter merge when incoming data is partial (e.g. stupid backbone) + // w/o this partial models from the server will fill in missing data with model defaults + // and overwrite existing data on the client + // see Backbone.Collection.set and _prepareModel + var collection = this; + models = _.map( models, function( model ){ + var existing = collection.get( model.id ); + if( !existing ){ return model; } + + // merge the models _BEFORE_ calling the superclass version + var merged = existing.toJSON(); + _.extend( merged, model ); + return merged; + }); + // now call superclass when the data is filled + Backbone.Collection.prototype.set.call( this, models, options ); + }, + + /** String representation. */ + toString : function(){ + return ([ 'VisualizationCollection(', [ this.historyId, this.length ].join(), ')' ].join( '' )); + } +}); + + +//============================================================================== +//return { +// Visualization : Visualization, +// VisualizationCollection : VisualizationCollection +//};}); diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/mvc/visualizations/scatterplotControlForm.js --- a/static/scripts/mvc/visualizations/scatterplotControlForm.js +++ /dev/null @@ -1,631 +0,0 @@ -/* ============================================================================= -todo: - I'd like to move the svg creation out of the splot constr. to: - allow adding splots to an existing canvas - allow mult. splots sharing a canvas - - - outside this: - BUG: setting width, height in plot controls doesn't re-interpolate data locations!! - BUG?: get metadata_column_names (from datatype if necessary) - BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations' - - wire label setters, anim setter - - TwoVarScatterplot: - ??: maybe better to do this with a canvas... - save as visualization - to seperate file? - remove underscore dependencies - add interface to change values (seperate)? - download svg -> base64 encode - incorporate glyphs, glyph state renderers - - ScatterplotSettingsForm: - some css bug that lowers the width of settings form when plot-controls tab is open - causes chart to shift - what can be abstracted/reused for other graphs? - avoid direct manipulation of this.plot - allow option to put plot into seperate tab of interface (for small multiples) - - provide callback in view to load data incrementally - for large sets - paginate - handle rerender - use endpoint (here and on the server (fileptr)) - fetch (new?) data - handle rerender - use d3.TSV? - render warning on long data (> maxDataPoints) - adjust endpoint - - selectable list of preset column comparisons (rnaseq etc.) - how to know what sort of Tabular the data is? - smarter about headers - validate columns selection (here or server) - - set stats column names by selected columns - move chart into tabbed area... - - Scatterplot.mako: - multiple plots on one page (small multiples) - ?? ensure svg styles thru d3 or css? - d3: configable (easily) - css: standard - better maintenance - ? override at config - -============================================================================= */ -/** - * Scatterplot control UI as a backbone view - * handles: - * getting the desired data - * configuring the plot display - * showing (general) statistics - * - * initialize attributes REQUIRES a dataset and an apiDatasetsURL - */ -var ScatterplotControlForm = Backbone.View.extend( LoggableMixin ).extend({ - //logger : console, - className : 'scatterplot-control-form', - - //NOTE: should include time needed to render - dataLoadDelay : 4000, - dataLoadSize : 5000, - - loadingIndicatorImage : 'loading_small_white_bg.gif', - fetchMsg : 'Fetching data...', - renderMsg : 'Rendering...', - - initialize : function( attributes ){ - this.log( this + '.initialize, attributes:', attributes ); - - this.dataset = null; - this.chartConfig = null; - this.chart = null; - this.loader = null; - - // set up refs to the four tab areas - this.$dataControl = null; - this.$chartControl = null; - this.$statsDisplay = null; - this.$chartDisplay = null; - - this.dataFetch = null; - - this.initializeFromAttributes( attributes ); - this.initializeChart( attributes ); - this.initializeDataLoader( attributes ); - }, - - initializeFromAttributes : function( attributes ){ - // required settings: ensure certain vars we need are passed in attributes - if( !attributes || !attributes.dataset ){ - throw( "ScatterplotView requires a dataset" ); - } else { - this.dataset = attributes.dataset; - } - if( jQuery.type( this.dataset.metadata_column_types ) === 'string' ){ - this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' ); - } - this.log( '\t dataset:', this.dataset ); - - // attempt to get possible headers from the data's first line - if( this.dataset.comment_lines && this.dataset.comment_lines.length ){ - //TODO:?? - var firstLine = this.dataset.comment_lines[0], - possibleHeaders = firstLine.split( '\t' ); - if( possibleHeaders.length === this.dataset.metadata_column_types.length ){ - this.possibleHeaders = possibleHeaders; - } - } - - // passed from mako helper - //TODO: integrate to galaxyPaths - //TODO: ?? seems like data loader section would be better - if( !attributes.apiDatasetsURL ){ - throw( "ScatterplotView requires a apiDatasetsURL" ); - } else { - this.dataURL = attributes.apiDatasetsURL + '/' + this.dataset.id + '?'; - } - this.log( '\t dataURL:', this.dataURL ); - }, - - initializeChart : function( attributes ){ - // set up the basic chart infrastructure and config (if any) - this.chartConfig = attributes.chartConfig || {}; - //if( this.logger ){ this.chartConfig.debugging = true; } - this.log( '\t initial chartConfig:', this.chartConfig ); - - this.chart = new TwoVarScatterplot( this.chartConfig ); - //TODO: remove 2nd ref, use this.chart.config - this.chartConfig = this.chart.config; - }, - - initializeDataLoader : function( attributes ){ - // set up data loader - var view = this; - this.loader = new LazyDataLoader({ - //logger : ( this.logger )?( this.logger ):( null ), - // we'll generate this when columns are chosen - url : null, - start : attributes.start || 0, - //NOTE: metadata_data_lines can be null (so we won't know the total) - total : attributes.total || this.dataset.metadata_data_lines, - delay : this.dataLoadDelay, - size : this.dataLoadSize, - - buildUrl : function( start, size ){ - // currently VERY SPECIFIC to using data_providers.py start_val, max_vals params - return this.url + '&' + jQuery.param({ - start_val: start, - max_vals: size - }); - } - }); - $( this.loader ).bind( 'error', function( event, status, error ){ - view.log( 'ERROR:', status, error ); - alert( 'ERROR fetching data:\n' + status + '\n' + error ); - view.hideLoadingIndicator(); - }); - }, - - // ------------------------------------------------------------------------- CONTROLS RENDERING - render : function(){ - this.log( this + '.render' ); - - // render the tab controls, areas and loading indicator - this.$el.append( ScatterplotControlForm.templates.mainLayout({ - loadingIndicatorImagePath : galaxy_config.root + 'static/images/' + this.loadingIndicatorImage, - message : '' - })); - - // render the tab content - this.$dataControl = this._render_dataControl(); - this.$chartControl = this._render_chartControl(); - this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' ); - this.$chartDisplay = this._render_chartDisplay(); - - // auto render if given both x, y column choices in query for page - //TODO:?? add autoRender=1 to query maybe? - if( this.chartConfig.xColumn && this.chartConfig.yColumn ){ - this.renderChart(); - } - - // set up behaviours - this.$el.find( '[title]' ).tooltip(); - - // uncomment any of the following to have that tab show on initial load (for testing) - //this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' ); - //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); - return this; - }, - - _render_dataControl : function(){ - // controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) - var view = this, - allColumns = [], - numericColumns = [], - usePossibleHeaders = ( this.possibleHeaders && this.$dataControl )? - ( this.$dataControl.find( '#first-line-header-checkbox' ).is( ':checked' ) ):( false ); - - // gather column indeces (from metadata_column_types) and names (from metadata_columnnames) - _.each( this.dataset.metadata_column_types, function( type, index ){ - // use a 1 based index in names/values within the form (will be dec. when parsed out) - var oneBasedIndex = index + 1, - // default name is 'column <index>'... - name = 'column ' + oneBasedIndex; - - // ...but label with the name if available... - if( view.dataset.metadata_column_names ){ - name = view.dataset.metadata_column_names[ index ]; - - // ...or, use the first line as headers if the user wants - } else if( usePossibleHeaders ){ - name = view.possibleHeaders[ index ]; - } - - // cache all columns here - allColumns.push({ index: oneBasedIndex, name: name }); - - // filter numeric columns to their own list - if( type === 'int' || type === 'float' ){ - numericColumns.push({ index: oneBasedIndex, name: name }); - } - }); - //TODO: other vals: max_vals, start_val, pagination (chart-settings) - - // render the html - var $dataControl = this.$el.find( '.tab-pane#data-control' ); - $dataControl.html( ScatterplotControlForm.templates.dataControl({ - allColumns : allColumns, - numericColumns : numericColumns, - possibleHeaders : ( this.possibleHeaders )?( this.possibleHeaders.join( ', ' ) ):( '' ), - usePossibleHeaders : usePossibleHeaders - })); - - if( !this.dataset.metadata_column_names && this.possibleHeaders ){ - $dataControl.find( '#first-line-header' ).show(); - } - - // preset to column selectors if they were passed in the config in the query string - $dataControl.find( '#X-select' ).val( this.chartConfig.xColumn ); - $dataControl.find( '#Y-select' ).val( this.chartConfig.yColumn ); - if( this.chartConfig.idColumn !== undefined ){ - $dataControl.find( '#include-id-checkbox' ) - .attr( 'checked', true ).trigger( 'change' ); - $dataControl.find( '#ID-select' ).val( this.chartConfig.idColumn ); - } - - return $dataControl; - }, - - _render_chartControl : function(){ - // tab content to control how the chart is rendered (data glyph size, chart size, etc.) - var view = this, - $chartControl = this.$el.find( '.tab-pane#chart-control' ), - // limits for controls (by control/chartConfig id) - //TODO: move into TwoVarScatterplot - controlRanges = { - 'datapointSize' : { min: 2, max: 10, step: 1 }, - 'width' : { min: 200, max: 800, step: 20 }, - 'height' : { min: 200, max: 800, step: 20 } - }; - - // render the html - $chartControl.append( ScatterplotControlForm.templates.chartControl( this.chartConfig ) ); - - // set up behaviours, js on sliders - $chartControl.find( '.numeric-slider-input' ).each( function(){ - var $this = $( this ), - $output = $this.find( '.slider-output' ), - $slider = $this.find( '.slider' ), - id = $this.attr( 'id' ); - //chartControl.log( 'slider set up', 'this:', $this, 'slider:', $slider, 'id', id ); - - // what to do when the slider changes: update display and update chartConfig - //TODO: move out of loop - function onSliderChange(){ - var $this = $( this ), - newValue = $this.slider( 'value' ); - //chartControl.log( 'slider change', 'this:', $this, 'output:', $output, 'value', newValue ); - $output.text( newValue ); - //chartControl.chartConfig[ id ] = newValue; - } - - $slider.slider( _.extend( controlRanges[ id ], { - value : view.chartConfig[ id ], - change : onSliderChange, - slide : onSliderChange - })); - }); - - return $chartControl; - }, - - _render_chartDisplay : function(){ - // render the tab content where the chart is displayed (but not the chart itself) - var $chartDisplay = this.$el.find( '.tab-pane#chart-display' ); - $chartDisplay.append( ScatterplotControlForm.templates.chartDisplay( this.chartConfig ) ); - return $chartDisplay; - }, - - // ------------------------------------------------------------------------- EVENTS - events : { - 'change #include-id-checkbox' : 'toggleThirdColumnSelector', - 'change #first-line-header-checkbox' : 'rerenderDataControl', - 'click #data-control #render-button' : 'renderChart', - 'click #chart-control #render-button' : 'changeChartSettings' - }, - - toggleThirdColumnSelector : function(){ - // show/hide the id selector on the data settings panel - this.$el.find( 'select[name="ID"]' ).parent().toggle(); - }, - - rerenderDataControl : function(){ - this.$dataControl = this._render_dataControl(); - }, - - showLoadingIndicator : function( message, callback ){ - // display the loading indicator over the tab panels if hidden, update message (if passed) - message = message || ''; - var indicator = this.$el.find( 'div#loading-indicator' ); - messageBox = indicator.find( '.loading-message' ); - - if( indicator.is( ':visible' ) ){ - if( message ){ - messageBox.fadeOut( 'fast', function(){ - messageBox.text( message ); - messageBox.fadeIn( 'fast', callback ); - }); - } else { - callback(); - } - - } else { - if( message ){ messageBox.text( message ); } - indicator.fadeIn( 'fast', callback ); - } - }, - - hideLoadingIndicator : function( callback ){ - this.$el.find( 'div#loading-indicator' ).fadeOut( 'fast', callback ); - }, - - // ------------------------------------------------------------------------- CHART/STATS RENDERING - renderChart : function(){ - // fetch the data, (re-)render the chart - this.log( this + '.renderChart' ); - - //TODO: separate data fetch - - // this is a complete re-render, so clear the prev. data - this.data = null; - this.meta = null; - - // update the chartConfig (here and chart) using chart settings - //TODO: separate and improve (used in changeChartSettings too) - _.extend( this.chartConfig, this.getChartSettings() ); - this.log( '\t chartConfig:', this.chartConfig ); - this.chart.updateConfig( this.chartConfig, false ); - - // build the url with the current data settings - this.loader.url = this.dataURL + '&' + jQuery.param( this.getDataSettings() ); - this.log( '\t loader: total lines:', this.loader.total, ' url:', this.loader.url ); - - // bind the new data event to: aggregate data, update the chart and stats with new data - var view = this; - $( this.loader ).bind( 'loaded.new', function( event, response ){ - view.log( view + ' loaded.new', response ); - - // aggregate data and meta - view.postProcessDataFetchResponse( response ); - view.log( '\t postprocessed data:', view.data ); - view.log( '\t postprocessed meta:', view.meta ); - - // update the chart and stats - view.showLoadingIndicator( view.renderMsg, function(){ - view.chart.render( view.data, view.meta ); - view.renderStats( view.data, view.meta ); - view.hideLoadingIndicator(); - }); - }); - // when all data loaded - unbind (or we'll start doubling event handlers) - $( this.loader ).bind( 'complete', function( event, data ){ - view.log( view + ' complete', data ); - $( view.loader ).unbind(); - }); - - // begin loading the data, switch to the chart display tab - view.showLoadingIndicator( view.fetchMsg, function(){ - view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); - view.loader.load(); - }); - }, - - renderStats : function(){ - this.log( this + '.renderStats' ); - // render the stats table in the stats panel - //TODO: there's a better way - this.$statsDisplay.html( ScatterplotControlForm.templates.statsDisplay({ - stats: [ - { name: 'Count', xval: this.meta[0].count, yval: this.meta[1].count }, - { name: 'Min', xval: this.meta[0].min, yval: this.meta[1].min }, - { name: 'Max', xval: this.meta[0].max, yval: this.meta[1].max }, - { name: 'Sum', xval: this.meta[0].sum, yval: this.meta[1].sum }, - { name: 'Mean', xval: this.meta[0].mean, yval: this.meta[1].mean }, - { name: 'Median', xval: this.meta[0].median, yval: this.meta[1].median } - ] - })); - }, - - changeChartSettings : function(){ - // re-render the chart with new chart settings and OLD data - var view = this; - newChartSettings = this.getChartSettings(); - - // update the chart config from the chartSettings panel controls - _.extend( this.chartConfig, newChartSettings ); - this.log( 'this.chartConfig:', this.chartConfig ); - this.chart.updateConfig( this.chartConfig, false ); - - // if there's current data, call chart.render with it (no data fetch) - if( view.data && view.meta ){ - view.showLoadingIndicator( view.renderMsg, function(){ - view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); - view.chart.render( view.data, view.meta ); - view.hideLoadingIndicator(); - }); - - // no current data, call renderChart instead (which will fetch data) - } else { - this.renderChart(); - } - }, - - // ------------------------------------------------------------------------- DATA AGGREGATION - postProcessDataFetchResponse : function( response ){ - // the loader only returns new data - it's up to this to munge the fetches together properly - //TODO: we're now storing data in two places: loader and here - // can't we reduce incoming data into loader.data[0]? are there concurrency problems? - this.postProcessData( response.data ); - this.postProcessMeta( response.meta ); - }, - - postProcessData : function( newData ){ - // stack the column data on top of each other into this.data - //this.log( this + '.postProcessData:', newData ); - var view = this; - - // if we already have data: aggregate - if( view.data ){ - _.each( newData, function( newColData, colIndex ){ - //view.log( colIndex + ' data:', newColData ); - //TODO??: time, space efficiency of this? - view.data[ colIndex ] = view.data[ colIndex ].concat( newColData ); - }); - - // otherwise: assign (first load) - } else { - view.data = newData; - } - }, - - postProcessMeta : function( newMeta ){ - // munge the meta data (stats) from the server fetches together - //pre: this.data must be preprocessed (needed for medians) - //this.log( this + '.postProcessMeta:', newMeta ); - var view = this, - colTypes = this.dataset.metadata_column_types; - - // if we already have meta: aggregate - if( view.meta ){ - _.each( newMeta, function( newColMeta, colIndex ){ - var colMeta = view.meta[ colIndex ], - colType = colTypes[ colIndex ]; - //view.log( '\t ' + colIndex + ' postprocessing meta:', newColMeta ); - //view.log( colIndex + ' old meta:', - // 'min:', colMeta.min, - // 'max:', colMeta.max, - // 'sum:', colMeta.sum, - // 'mean:', colMeta.mean, - // 'median:', colMeta.median - //); - - //!TODO: at what point are we getting int/float overflow on these?! - //??: need to be null safe? - colMeta.count += ( newColMeta.count )?( newColMeta.count ):( 0 ); - //view.log( colIndex, 'count:', colMeta.count ); - - if( ( colType === 'int' ) || ( colType === 'float' ) ){ - //view.log( colIndex + ' incoming meta:', - // 'min:', newColMeta.min, - // 'max:', newColMeta.max, - // 'sum:', newColMeta.sum, - // 'mean:', newColMeta.mean, - // 'median:', newColMeta.median - //); - - colMeta.min = Math.min( newColMeta.min, colMeta.min ); - colMeta.max = Math.max( newColMeta.max, colMeta.max ); - colMeta.sum = newColMeta.sum + colMeta.sum; - colMeta.mean = ( colMeta.count )?( colMeta.sum / colMeta.count ):( null ); - - // median's a pain bc of sorting (requires the data as well) - var sortedCol = view.data[ colIndex ].slice().sort(), - middleIndex = Math.floor( sortedCol.length / 2 ); - - if( sortedCol.length % 2 === 0 ){ - colMeta.median = ( ( sortedCol[ middleIndex ] + sortedCol[( middleIndex + 1 )] ) / 2 ); - - } else { - colMeta.median = sortedCol[ middleIndex ]; - } - - //view.log( colIndex + ' new meta:', - // 'min:', colMeta.min, - // 'max:', colMeta.max, - // 'sum:', colMeta.sum, - // 'mean:', colMeta.mean, - // 'median:', colMeta.median - //); - } - }); - - // otherwise: assign (first load) - } else { - view.meta = newMeta; - //view.log( '\t meta (first load):', view.meta ); - } - }, - - // ------------------------------------------------------------------------- GET DATA/CHART SETTINGS - getDataSettings : function(){ - // parse the column values for both indeces (for the data fetch) and names (for the chart) - var columnSelections = this.getColumnSelections(), - columns = []; - this.log( '\t columnSelections:', columnSelections ); - - //TODO: validate columns - minimally: we can assume either set by selectors or via a good query string - - // get column indices for params, include the desired ID column (if any) - //NOTE: these are presented in human-readable 1 base index (to match the data.peek) - adjust - columns = [ - columnSelections.X.colIndex - 1, - columnSelections.Y.colIndex - 1 - ]; - if( this.$dataControl.find( '#include-id-checkbox' ).attr( 'checked' ) ){ - columns.push( columnSelections.ID.colIndex - 1 ); - } - //TODO: other vals: max, start, page - - var params = { - data_type : 'raw_data', - provider : 'column_with_stats', - columns : '[' + columns + ']' - }; - this.log( '\t data settings (url params):', params ); - return params; - }, - - getColumnSelections : function(){ - // gets the current user-selected values for which columns to fetch from the data settings panel - // returns a map: { column-select name (eg. X) : { colIndex : column-selector val, - // colName : selected option text }, ... } - var selections = {}; - this.$dataControl.find( 'div.column-select select' ).each( function(){ - var $this = $( this ), - val = $this.val(); - selections[ $this.attr( 'name' ) ] = { - colIndex : val, - colName : $this.children( '[value="' + val + '"]' ).text() - }; - }); - return selections; - }, - - getChartSettings : function(){ - // gets the user-selected chartConfig from the chart settings panel - var settings = {}, - colSelections = this.getColumnSelections(); - //this.log( 'colSelections:', colSelections ); - - //TODO: simplify with keys and loop - settings.datapointSize = this.$chartControl.find( '#datapointSize.numeric-slider-input' ) - .find( '.slider' ).slider( 'value' ); - settings.width = this.$chartControl.find( '#width.numeric-slider-input' ) - .find( '.slider' ).slider( 'value' ); - settings.height = this.$chartControl.find( '#height.numeric-slider-input' ) - .find( '.slider' ).slider( 'value' ); - - // update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName - //TODO: a little confusing - var chartSettingsXLabel = this.$chartControl.find( 'input#X-axis-label' ).val(), - chartSettingsYLabel = this.$chartControl.find( 'input#Y-axis-label' ).val(); - settings.xLabel = ( chartSettingsXLabel === 'X' )? - ( colSelections.X.colName ):( chartSettingsXLabel ); - settings.yLabel = ( chartSettingsYLabel === 'Y' )? - ( colSelections.Y.colName ):( chartSettingsYLabel ); - - settings.animDuration = ( this.$chartControl.find( '#animate-chart' ).is( ':checked' ) )? - ( this.chart.defaults.animDuration ):( 0 ); - - this.log( '\t chartSettings:', settings ); - return settings; - }, - - toString : function(){ - return 'ScatterplotControlForm(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')'; - } -}); - -ScatterplotControlForm.templates = { - mainLayout : Handlebars.templates[ 'template-visualization-scatterplotControlForm' ], - dataControl : Handlebars.templates[ 'template-visualization-dataControl' ], - chartControl : Handlebars.templates[ 'template-visualization-chartControl' ], - statsDisplay : Handlebars.templates[ 'template-visualization-statsDisplay' ], - chartDisplay : Handlebars.templates[ 'template-visualization-chartDisplay' ] -}; - -//============================================================================== diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/packed/mvc/dataset/hda-edit.js --- a/static/scripts/packed/mvc/dataset/hda-edit.js +++ b/static/scripts/packed/mvc/dataset/hda-edit.js @@ -1,1 +1,1 @@ -define(["mvc/dataset/hda-model","mvc/dataset/hda-base"],function(d,a){var f=a.HDABaseView.extend(LoggableMixin).extend({initialize:function(g){a.HDABaseView.prototype.initialize.call(this,g);this.hasUser=g.hasUser;this.defaultPrimaryActionButtonRenderers=[this._render_showParamsButton,this._render_rerunButton];this.tagsEditorShown=g.tagsEditorShown||false;this.annotationEditorShown=g.annotationEditorShown||false},_render_titleButtons:function(){return a.HDABaseView.prototype._render_titleButtons.call(this).concat([this._render_editButton(),this._render_deleteButton()])},_render_editButton:function(){if((this.model.get("state")===d.HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.DISCARDED)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){return null}var i=this.model.get("purged"),g=this.model.get("deleted"),h={title:_l("Edit attributes"),href:this.urls.edit,target:this.linkTarget,classes:"dataset-edit"};if(g||i){h.disabled=true;if(i){h.title=_l("Cannot edit attributes of datasets removed from disk")}else{if(g){h.title=_l("Undelete dataset to edit attributes")}}}else{if(this.model.get("state")===d.HistoryDatasetAssociation.STATES.UPLOAD){h.disabled=true;h.title=_l("This dataset must finish uploading before it can be edited")}}h.faIcon="fa-pencil";return faIconButton(h)},_render_deleteButton:function(){if((this.model.get("state")===d.HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){return null}var g=this,h={title:_l("Delete"),classes:"dataset-delete",onclick:function(){g.$el.find(".icon-btn.dataset-delete").trigger("mouseout");g.model["delete"]()}};if(this.model.get("deleted")||this.model.get("purged")){h={title:_l("Dataset is already deleted"),disabled:true}}h.faIcon="fa-times";return faIconButton(h)},_render_errButton:function(){if(this.model.get("state")!==d.HistoryDatasetAssociation.STATES.ERROR){return null}return faIconButton({title:_l("View or report this error"),href:this.urls.report_error,classes:"dataset-report-error-btn",target:this.linkTarget,faIcon:"fa-bug"})},_render_rerunButton:function(){return faIconButton({title:_l("Run this job again"),href:this.urls.rerun,classes:"dataset-rerun-btn",target:this.linkTarget,faIcon:"fa-refresh"})},_render_visualizationsButton:function(){var n=this.model.get("visualizations");if((!this.hasUser)||(!this.model.hasData())||(_.isEmpty(n))){return null}if(_.isObject(n[0])){return this._render_visualizationsFrameworkButton(n)}if(!this.urls.visualization){return null}var k=this.model.get("dbkey"),g=this.urls.visualization,j={},h={dataset_id:this.model.get("id"),hda_ldda:"hda"};if(k){h.dbkey=k}var l=faIconButton({title:_l("Visualize"),classes:"dataset-visualize-btn",faIcon:"fa-bar-chart-o"});var m=this;function i(p){switch(p){case"trackster":return b(g,h,k);case"scatterplot":return e(g,h,m.linkTarget);default:return function(){Galaxy.frame.add({title:"Visualization",type:"url",content:g+"/"+p+"?"+$.param(h)})}}}function o(p){return p.charAt(0).toUpperCase()+p.slice(1)}if(n.length===1){l.attr("data-original-title",_l("Visualize in ")+_l(o(n[0])));l.click(i(n[0]))}else{_.each(n,function(p){j[_l(o(p))]=i(p)});make_popupmenu(l,j)}return l},_render_visualizationsFrameworkButton:function(g){if(!(this.model.hasData())||!(g&&!_.isEmpty(g))){return null}var i=faIconButton({title:_l("Visualize"),classes:"dataset-visualize-btn",faIcon:"fa-bar-chart-o"});i.addClass("visualize-icon");if(_.keys(g).length===1){i.attr("title",_.keys(g)[0]);i.attr("href",_.values(g)[0])}else{var j=[];_.each(g,function(k){j.push(k)});var h=new PopupMenu(i,j)}return i},_buildNewRender:function(){var g=a.HDABaseView.prototype._buildNewRender.call(this);g.find(".dataset-deleted-msg").append(_l('Click <a href="javascript:void(0);" class="dataset-undelete">here</a> to undelete it or <a href="javascript:void(0);" class="dataset-purge">here</a> to immediately remove it from disk'));g.find(".dataset-hidden-msg").append(_l('Click <a href="javascript:void(0);" class="dataset-unhide">here</a> to unhide it'));return g},_render_body_failed_metadata:function(){var h=$("<a/>").attr({href:this.urls.edit,target:this.linkTarget}).text(_l("set it manually or retry auto-detection")),g=$("<span/>").text(". "+_l("You may be able to")+" ").append(h),i=a.HDABaseView.prototype._render_body_failed_metadata.call(this);i.find(".warningmessagesmall strong").append(g);return i},_render_body_error:function(){var g=a.HDABaseView.prototype._render_body_error.call(this);g.find(".dataset-actions .left").prepend(this._render_errButton());return g},_render_body_ok:function(){var g=a.HDABaseView.prototype._render_body_ok.call(this);if(this.model.isDeletedOrPurged()){return g}this.makeDbkeyEditLink(g);if(this.hasUser){g.find(".dataset-actions .left").append(this._render_visualizationsButton());this._renderTags(g);this._renderAnnotation(g)}return g},_renderTags:function(g){var h=this;this.tagsEditor=new TagsEditor({model:this.model,el:g.find(".tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){h.tagsEditorShown=true},onhide:function(){h.tagsEditorShown=false},$activator:faIconButton({title:_l("Edit dataset tags"),classes:"dataset-tag-btn",faIcon:"fa-tags"}).appendTo(g.find(".dataset-actions .right"))});if(this.tagsEditorShown){this.tagsEditor.toggle(true)}},_renderAnnotation:function(g){var h=this;this.annotationEditor=new AnnotationEditor({model:this.model,el:g.find(".annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){h.annotationEditorShown=true},onhide:function(){h.annotationEditorShown=false},$activator:faIconButton({title:_l("Edit dataset annotation"),classes:"dataset-annotate-btn",faIcon:"fa-comment"}).appendTo(g.find(".dataset-actions .right"))});if(this.annotationEditorShown){this.annotationEditor.toggle(true)}},makeDbkeyEditLink:function(h){if(this.model.get("metadata_dbkey")==="?"&&!this.model.isDeletedOrPurged()){var g=$('<a class="value">?</a>').attr("href",this.urls.edit).attr("target",this.linkTarget);h.find(".dataset-dbkey .value").replaceWith(g)}},events:_.extend(_.clone(a.HDABaseView.prototype.events),{"click .dataset-undelete":function(g){this.model.undelete();return false},"click .dataset-unhide":function(g){this.model.unhide();return false},"click .dataset-purge":"confirmPurge"}),confirmPurge:function c(g){this.model.purge();return false},toString:function(){var g=(this.model)?(this.model+""):("(no model)");return"HDAView("+g+")"}});function e(g,i,h){action=function(){Galaxy.frame.add({title:"Scatterplot",type:"url",content:g+"/scatterplot?"+$.param(i),target:h,scratchbook:true});$("div.popmenu-wrapper").remove();return false};return action}function b(g,i,h){return function(){var j={};if(h){j["f-dbkey"]=h}$.ajax({url:g+"/list_tracks?"+$.param(j),dataType:"html",error:function(){alert(("Could not add this dataset to browser")+".")},success:function(k){var l=window.parent;l.Galaxy.modal.show({title:"View Data in a New or Saved Visualization",buttons:{Cancel:function(){l.Galaxy.modal.hide()},"View in saved visualization":function(){l.Galaxy.modal.show({title:"Add Data to Saved Visualization",body:k,buttons:{Cancel:function(){l.Galaxy.modal.hide()},"Add to visualization":function(){$(l.document).find("input[name=id]:checked").each(function(){l.Galaxy.modal.hide();var m=$(this).val();i.id=m;l.Galaxy.frame.add({title:"Trackster",type:"url",content:g+"/trackster?"+$.param(i),scratchbook:true})})}}})},"View in new visualization":function(){l.Galaxy.modal.hide();var m=g+"/trackster?"+$.param(i);l.Galaxy.frame.add({title:"Trackster",type:"url",content:m,scratchbook:true})}}})}});return false}}return{HDAEditView:f}}); \ No newline at end of file +define(["mvc/dataset/hda-model","mvc/dataset/hda-base"],function(d,a){var f=a.HDABaseView.extend(LoggableMixin).extend({initialize:function(g){a.HDABaseView.prototype.initialize.call(this,g);this.hasUser=g.hasUser;this.defaultPrimaryActionButtonRenderers=[this._render_showParamsButton,this._render_rerunButton];this.tagsEditorShown=g.tagsEditorShown||false;this.annotationEditorShown=g.annotationEditorShown||false},_render_titleButtons:function(){return a.HDABaseView.prototype._render_titleButtons.call(this).concat([this._render_editButton(),this._render_deleteButton()])},_render_editButton:function(){if((this.model.get("state")===d.HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.DISCARDED)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){return null}var i=this.model.get("purged"),g=this.model.get("deleted"),h={title:_l("Edit attributes"),href:this.urls.edit,target:this.linkTarget,classes:"dataset-edit"};if(g||i){h.disabled=true;if(i){h.title=_l("Cannot edit attributes of datasets removed from disk")}else{if(g){h.title=_l("Undelete dataset to edit attributes")}}}else{if(this.model.get("state")===d.HistoryDatasetAssociation.STATES.UPLOAD){h.disabled=true;h.title=_l("This dataset must finish uploading before it can be edited")}}h.faIcon="fa-pencil";return faIconButton(h)},_render_deleteButton:function(){if((this.model.get("state")===d.HistoryDatasetAssociation.STATES.NEW)||(this.model.get("state")===d.HistoryDatasetAssociation.STATES.NOT_VIEWABLE)||(!this.model.get("accessible"))){return null}var g=this,h={title:_l("Delete"),classes:"dataset-delete",onclick:function(){g.$el.find(".icon-btn.dataset-delete").trigger("mouseout");g.model["delete"]()}};if(this.model.get("deleted")||this.model.get("purged")){h={title:_l("Dataset is already deleted"),disabled:true}}h.faIcon="fa-times";return faIconButton(h)},_render_errButton:function(){if(this.model.get("state")!==d.HistoryDatasetAssociation.STATES.ERROR){return null}return faIconButton({title:_l("View or report this error"),href:this.urls.report_error,classes:"dataset-report-error-btn",target:this.linkTarget,faIcon:"fa-bug"})},_render_rerunButton:function(){return faIconButton({title:_l("Run this job again"),href:this.urls.rerun,classes:"dataset-rerun-btn",target:this.linkTarget,faIcon:"fa-refresh"})},_render_visualizationsButton:function(){var n=this.model.get("visualizations");if((!this.hasUser)||(!this.model.hasData())||(_.isEmpty(n))){return null}if(_.isObject(n[0])){return this._render_visualizationsFrameworkButton(n)}if(!this.urls.visualization){return null}var k=this.model.get("dbkey"),g=this.urls.visualization,j={},h={dataset_id:this.model.get("id"),hda_ldda:"hda"};if(k){h.dbkey=k}var l=faIconButton({title:_l("Visualize"),classes:"dataset-visualize-btn",faIcon:"fa-bar-chart-o"});var m=this;function i(p){switch(p){case"trackster":return b(g,h,k);case"scatterplot":return e(g,h,m.linkTarget);default:return function(){Galaxy.frame.add({title:"Visualization",type:"url",content:g+"/"+p+"?"+$.param(h)})}}}function o(p){return p.charAt(0).toUpperCase()+p.slice(1)}if(n.length===1){l.attr("data-original-title",_l("Visualize in ")+_l(o(n[0])));l.click(i(n[0]))}else{_.each(n,function(p){j[_l(o(p))]=i(p)});make_popupmenu(l,j)}return l},_render_visualizationsFrameworkButton:function(g){if(!(this.model.hasData())||!(g&&!_.isEmpty(g))){return null}var h=faIconButton({title:_l("Visualize"),classes:"dataset-visualize-btn",faIcon:"fa-bar-chart-o"});if(_.keys(g).length===1){h.attr("title",_.keys(g)[0]);h.attr("href",_.values(g)[0])}else{var i=[];_.each(g,function(j){j.func=function(){if(Galaxy.frame.active){Galaxy.frame.add({title:"Visualization",type:"url",content:j.href});return false}return true};i.push(j);return false});PopupMenu.create(h,i)}return h},_buildNewRender:function(){var g=a.HDABaseView.prototype._buildNewRender.call(this);g.find(".dataset-deleted-msg").append(_l('Click <a href="javascript:void(0);" class="dataset-undelete">here</a> to undelete it or <a href="javascript:void(0);" class="dataset-purge">here</a> to immediately remove it from disk'));g.find(".dataset-hidden-msg").append(_l('Click <a href="javascript:void(0);" class="dataset-unhide">here</a> to unhide it'));return g},_render_body_failed_metadata:function(){var h=$("<a/>").attr({href:this.urls.edit,target:this.linkTarget}).text(_l("set it manually or retry auto-detection")),g=$("<span/>").text(". "+_l("You may be able to")+" ").append(h),i=a.HDABaseView.prototype._render_body_failed_metadata.call(this);i.find(".warningmessagesmall strong").append(g);return i},_render_body_error:function(){var g=a.HDABaseView.prototype._render_body_error.call(this);g.find(".dataset-actions .left").prepend(this._render_errButton());return g},_render_body_ok:function(){var g=a.HDABaseView.prototype._render_body_ok.call(this);if(this.model.isDeletedOrPurged()){return g}this.makeDbkeyEditLink(g);if(this.hasUser){g.find(".dataset-actions .left").append(this._render_visualizationsButton());this._renderTags(g);this._renderAnnotation(g)}return g},_renderTags:function(g){var h=this;this.tagsEditor=new TagsEditor({model:this.model,el:g.find(".tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){h.tagsEditorShown=true},onhide:function(){h.tagsEditorShown=false},$activator:faIconButton({title:_l("Edit dataset tags"),classes:"dataset-tag-btn",faIcon:"fa-tags"}).appendTo(g.find(".dataset-actions .right"))});if(this.tagsEditorShown){this.tagsEditor.toggle(true)}},_renderAnnotation:function(g){var h=this;this.annotationEditor=new AnnotationEditor({model:this.model,el:g.find(".annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){h.annotationEditorShown=true},onhide:function(){h.annotationEditorShown=false},$activator:faIconButton({title:_l("Edit dataset annotation"),classes:"dataset-annotate-btn",faIcon:"fa-comment"}).appendTo(g.find(".dataset-actions .right"))});if(this.annotationEditorShown){this.annotationEditor.toggle(true)}},makeDbkeyEditLink:function(h){if(this.model.get("metadata_dbkey")==="?"&&!this.model.isDeletedOrPurged()){var g=$('<a class="value">?</a>').attr("href",this.urls.edit).attr("target",this.linkTarget);h.find(".dataset-dbkey .value").replaceWith(g)}},events:_.extend(_.clone(a.HDABaseView.prototype.events),{"click .dataset-undelete":function(g){this.model.undelete();return false},"click .dataset-unhide":function(g){this.model.unhide();return false},"click .dataset-purge":"confirmPurge"}),confirmPurge:function c(g){this.model.purge();return false},toString:function(){var g=(this.model)?(this.model+""):("(no model)");return"HDAView("+g+")"}});function e(g,i,h){action=function(){Galaxy.frame.add({title:"Scatterplot",type:"url",content:g+"/scatterplot?"+$.param(i),target:h,scratchbook:true});$("div.popmenu-wrapper").remove();return false};return action}function b(g,i,h){return function(){var j={};if(h){j["f-dbkey"]=h}$.ajax({url:g+"/list_tracks?"+$.param(j),dataType:"html",error:function(){alert(("Could not add this dataset to browser")+".")},success:function(k){var l=window.parent;l.Galaxy.modal.show({title:"View Data in a New or Saved Visualization",buttons:{Cancel:function(){l.Galaxy.modal.hide()},"View in saved visualization":function(){l.Galaxy.modal.show({title:"Add Data to Saved Visualization",body:k,buttons:{Cancel:function(){l.Galaxy.modal.hide()},"Add to visualization":function(){$(l.document).find("input[name=id]:checked").each(function(){l.Galaxy.modal.hide();var m=$(this).val();i.id=m;l.Galaxy.frame.add({title:"Trackster",type:"url",content:g+"/trackster?"+$.param(i),scratchbook:true})})}}})},"View in new visualization":function(){l.Galaxy.modal.hide();var m=g+"/trackster?"+$.param(i);l.Galaxy.frame.add({title:"Trackster",type:"url",content:m,scratchbook:true})}}})}});return false}}return{HDAEditView:f}}); \ No newline at end of file diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 static/scripts/packed/mvc/ui.js --- a/static/scripts/packed/mvc/ui.js +++ b/static/scripts/packed/mvc/ui.js @@ -1,1 +1,1 @@ -var IconButton=Backbone.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:true,id:null,href:null,target:null,enabled:true,visible:true,tooltip_config:{}}});var IconButtonView=Backbone.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"};this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var a=this.template(this.model.toJSON());a.tooltip(this.model.get("tooltip_config"));this.$el.replaceWith(a);this.setElement(a);return this},events:{click:"click"},click:function(a){if(_.isFunction(this.model.get("on_click"))){this.model.get("on_click")(a);return false}return true},template:function(b){var a='title="'+b.title+'" class="icon-button';if(b.is_menu_button){a+=" menu-button"}a+=" "+b.icon_class;if(!b.enabled){a+="_disabled"}a+='"';if(b.id){a+=' id="'+b.id+'"'}a+=' href="'+b.href+'"';if(b.target){a+=' target="'+b.target+'"'}if(!b.visible){a+=' style="display: none;"'}if(b.enabled){a="<a "+a+"/>"}else{a="<span "+a+"/>"}return $(a)}});var IconButtonCollection=Backbone.Collection.extend({model:IconButton});var IconButtonMenuView=Backbone.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var a=this;this.collection.each(function(d){var b=$("<a/>").attr("href","javascript:void(0)").attr("title",d.attributes.title).addClass("icon-button menu-button").addClass(d.attributes.icon_class).appendTo(a.$el).click(d.attributes.on_click);if(d.attributes.tooltip_config){b.tooltip(d.attributes.tooltip_config)}var c=d.get("options");if(c){make_popupmenu(b,c)}});return this}});var create_icon_buttons_menu=function(b,a){if(!a){a={}}var c=new IconButtonCollection(_.map(b,function(d){return new IconButton(_.extend(d,a))}));return new IconButtonMenuView({collection:c})};var Grid=Backbone.Collection.extend({});var GridView=Backbone.View.extend({});var PopupMenu=Backbone.View.extend({initialize:function(b,a){this.$button=b;if(!this.$button.size()){this.$button=$("<div/>")}this.options=a||[];var c=this;this.$button.click(function(d){$(".popmenu-wrapper").remove();c._renderAndShow(d);return false})},_renderAndShow:function(a){this.render();this.$el.appendTo("body").css(this._getShownPosition(a)).show();this._setUpCloseBehavior()},render:function(){this.$el.addClass("popmenu-wrapper").hide().css({position:"absolute"}).html(this.template(this.$button.attr("id"),this.options));if(this.options.length){var a=this;this.$el.find("li").each(function(c,b){var d=a.options[c];if(d.func){$(this).children("a.popupmenu-option").click(function(e){d.func.call(a,e,d)})}})}return this},template:function(b,a){return['<ul id="',b,'-menu" class="dropdown-menu">',this._templateOptions(a),"</ul>"].join("")},_templateOptions:function(a){if(!a.length){return"<li>(no options)</li>"}return _.map(a,function(d){if(d.divider){return'<li class="divider"></li>'}else{if(d.header){return['<li class="head"><a href="javascript:void(0);">',d.html,"</a></li>"].join("")}}var c=d.href||"javascript:void(0);",e=(d.target)?(' target="'+d.target+'"'):(""),b=(d.checked)?('<span class="fa fa-check"></span>'):("");return['<li><a class="popupmenu-option" href="',c,'"',e,">",b,d.html,"</a></li>"].join("")}).join("")},_getShownPosition:function(b){var c=this.$el.width();var a=b.pageX-c/2;a=Math.min(a,$(document).scrollLeft()+$(window).width()-c-5);a=Math.max(a,$(document).scrollLeft()+5);return{top:b.pageY,left:a}},_setUpCloseBehavior:function(){var c=this;function a(e){$(document).off("click.close_popup");if(window.parent!==window){try{$(window.parent.document).off("click.close_popup")}catch(d){}}else{try{$("iframe#galaxy_main").contents().off("click.close_popup")}catch(d){}}c.remove()}$("html").one("click.close_popup",a);if(window.parent!==window){try{$(window.parent.document).find("html").one("click.close_popup",a)}catch(b){}}else{try{$("iframe#galaxy_main").contents().one("click.close_popup",a)}catch(b){}}},addItem:function(b,a){a=(a>=0)?a:this.options.length;this.options.splice(a,0,b);return this},removeItem:function(a){if(a>=0){this.options.splice(a,1)}return this},findIndexByHtml:function(b){for(var a=0;a<this.options.length;a++){if(_.has(this.options[a],"html")&&(this.options[a].html===b)){return a}}return null},findItemByHtml:function(a){return this.options[(this.findIndexByHtml(a))]},toString:function(){return"PopupMenu"}});PopupMenu.make_popupmenu=function(b,c){var a=[];_.each(c,function(f,d){var e={html:d};if(f===null){e.header=true}else{if(jQuery.type(f)==="function"){e.func=f}}a.push(e)});return new PopupMenu($(b),a)};PopupMenu.convertLinksToOptions=function(c,a){c=$(c);a=a||"a";var b=[];c.find(a).each(function(g,e){var f={},d=$(g);f.html=d.text();if(d.attr("href")){var j=d.attr("href"),k=d.attr("target"),h=d.attr("confirm");f.func=function(){if((h)&&(!confirm(h))){return}switch(k){case"_parent":window.parent.location=j;break;case"_top":window.top.location=j;break;default:window.location=j}}}b.push(f)});return b};PopupMenu.fromExistingDom=function(d,c,a){d=$(d);c=$(c);var b=PopupMenu.convertLinksToOptions(c,a);c.remove();return new PopupMenu(d,b)};PopupMenu.make_popup_menus=function(c,b,d){c=c||document;b=b||"div[popupmenu]";d=d||function(e,f){return"#"+e.attr("popupmenu")};var a=[];$(c).find(b).each(function(){var e=$(this),f=$(c).find(d(e,c));a.push(PopupMenu.fromDom(f,e));f.addClass("popup")});return a};var faIconButton=function(a){a=a||{};a.tooltipConfig=a.tooltipConfig||{placement:"bottom"};a.classes=["icon-btn"].concat(a.classes||[]);if(a.disabled){a.classes.push("disabled")}var b=['<a class="',a.classes.join(" "),'"',((a.title)?(' title="'+a.title+'"'):("")),((!a.disabled&&a.target)?(' target="'+a.target+'"'):("")),' href="',((!a.disabled&&a.href)?(a.href):("javascript:void(0);")),'">','<span class="fa ',a.faIcon,'"></span>',"</a>"].join("");var c=$(b).tooltip(a.tooltipConfig);if(_.isFunction(a.onclick)){c.click(a.onclick)}return c};(function(){function a(j,p){var d=27,m=13,c=$(j),e=true,g={initialVal:"",name:"search",placeholder:"search",classes:"",onclear:function(){},onfirstsearch:null,onsearch:function(q){},minSearchLen:0,escWillClear:true,oninit:function(){}};function i(q){var r=$(this).parent().children("input");r.val("");r.trigger("clear:searchInput");p.onclear()}function o(r,q){$(this).trigger("search:searchInput",q);if(typeof p.onfirstsearch==="function"&&e){e=false;p.onfirstsearch(q)}else{p.onsearch(q)}}function f(){return['<input type="text" name="',p.name,'" placeholder="',p.placeholder,'" ','class="search-query ',p.classes,'" ',"/>"].join("")}function l(){return $(f()).focus(function(q){$(this).select()}).keyup(function(r){if(r.which===d&&p.escWillClear){i.call(this,r)}else{var q=$(this).val();if((r.which===m)||(p.minSearchLen&&q.length>=p.minSearchLen)){o.call(this,r,q)}else{if(!q.length){i.call(this,r)}}}}).val(p.initialVal)}function k(){return $(['<span class="search-clear fa fa-times-circle" ','title="',_l("clear search (esc)"),'"></span>'].join("")).tooltip({placement:"bottom"}).click(function(q){i.call(this,q)})}function n(){return $(['<span class="search-loading fa fa-spinner fa-spin" ','title="',_l("loading..."),'"></span>'].join("")).hide().tooltip({placement:"bottom"})}function h(){c.find(".search-loading").toggle();c.find(".search-clear").toggle()}if(jQuery.type(p)==="string"){if(p==="toggle-loading"){h()}return c}if(jQuery.type(p)==="object"){p=jQuery.extend(true,{},g,p)}return c.addClass("search-input").prepend([l(),k(),n()])}jQuery.fn.extend({searchInput:function b(c){return this.each(function(){return a(this,c)})}})}());(function(){function b(m,l){this.currModeIndex=0;return this.init(m,l)}b.prototype.DATA_KEY="mode-button";b.prototype.defaults={modes:[{mode:"default"}]};b.prototype.init=function f(m,l){l=l||{};this.$element=$(m);this.options=jQuery.extend(true,{},this.defaults,l);var o=this;this.$element.click(function n(p){o.callModeFn();o._incModeIndex();$(this).html(o.options.modes[o.currModeIndex].html)});this.currModeIndex=0;if(this.options.initialMode){this.currModeIndex=this._getModeIndex(this.options.initialMode)}return this};b.prototype._getModeIndex=function j(l){for(var m=0;m<this.options.modes.length;m+=1){if(this.options.modes[m].mode===l){return m}}throw new Error("mode not found: "+l)};b.prototype.getCurrMode=function a(){return this.options.modes[this.currModeIndex]};b.prototype.getMode=function g(l){if(!l){return this.getCurrMode()}return this.options.modes[(this._getModeIndex(l))]};b.prototype.hasMode=function k(l){return !!this.getMode(l)};b.prototype.currentMode=function e(){return this.options.modes[this.currModeIndex]};b.prototype.setMode=function c(m){var l=this.getMode(m);this.$element.html(l.html||null);return this};b.prototype._incModeIndex=function d(){this.currModeIndex+=1;if(this.currModeIndex>=this.options.modes.length){this.currModeIndex=0}return this};b.prototype.callModeFn=function h(l){var m=this.getMode(l).onclick;if(m&&jQuery.type(m==="function")){return m.call(this)}return undefined};jQuery.fn.extend({modeButton:function i(m){var l=jQuery.makeArray(arguments).slice(1);return this.map(function(){var p=$(this),o=p.data("mode-button");if(jQuery.type(m)==="object"){o=new b(p,m);p.data("mode-button",o)}else{if(o&&jQuery.type(m)==="string"){var n=o[m];if(jQuery.type(n)==="function"){return n.apply(o,l)}}else{if(o){return o}}}return this})}})}());function LoadingIndicator(a,c){var b=this;c=jQuery.extend({cover:false},c||{});function d(){var e=['<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 g=$(e).hide().css(c.css||{position:"fixed"}),f=g.children(".loading-indicator-text");if(c.cover){g.css({"z-index":2,top:a.css("top"),bottom:a.css("bottom"),left:a.css("left"),right:a.css("right"),opacity:0.5,"background-color":"white","text-align":"center"});f=g.children(".loading-indicator-text").css({"margin-top":"20px"})}else{f=g.children(".loading-indicator-text").css({margin:"12px 0px 0px 10px",opacity:"0.85",color:"grey"});f.children(".loading-indicator-message").css({margin:"0px 8px 0px 0px","font-style":"italic"})}return g}b.show=function(f,e,g){f=f||"loading...";e=e||"fast";b.$indicator=d().insertBefore(a);b.message(f);b.$indicator.fadeIn(e,g);return b};b.message=function(e){b.$indicator.find("i").text(e)};b.hide=function(e,f){e=e||"fast";if(b.$indicator&&b.$indicator.size()){b.$indicator.fadeOut(e,function(){b.$indicator.remove();if(f){f()}})}else{if(f){f()}}return b};return b}function dropDownSelect(b,c){c=c||((!_.isEmpty(b))?(b[0]):(""));var a=$(['<div class="dropdown-select btn-group">','<button type="button" class="btn btn-default">','<span class="dropdown-select-selected">'+c+"</span>","</button>","</div>"].join("\n"));if(b&&b.length>1){a.find("button").addClass("dropdown-toggle").attr("data-toggle","dropdown").append(' <span class="caret"></span>');a.append(['<ul class="dropdown-menu" role="menu">',_.map(b,function(e){return['<li><a href="javascript:void(0)">',e,"</a></li>"].join("")}).join("\n"),"</ul>"].join("\n"))}function d(g){var h=$(this),f=h.parents(".dropdown-select"),e=h.text();f.find(".dropdown-select-selected").text(e);f.trigger("change.dropdown-select",e)}a.find("a").click(d);return a}(function(){function e(k,j){return this.init(k,j)}e.prototype.DATA_KEY="filter-control";e.prototype.init=function g(k,j){j=j||{filters:[]};this.$element=$(k).addClass("filter-control btn-group");this.options=jQuery.extend(true,{},this.defaults,j);this.currFilter=this.options.filters[0];return this.render()};e.prototype.render=function d(){this.$element.empty().append([this._renderKeySelect(),this._renderOpSelect(),this._renderValueInput()]);return this};e.prototype._renderKeySelect=function a(){var j=this;var k=this.options.filters.map(function(l){return l.key});this.$keySelect=dropDownSelect(k,this.currFilter.key).addClass("filter-control-key").on("change.dropdown-select",function(m,l){j.currFilter=_.findWhere(j.options.filters,{key:l});j.render()._triggerChange()});return this.$keySelect};e.prototype._renderOpSelect=function i(){var j=this,k=this.currFilter.ops;this.$opSelect=dropDownSelect(k,k[0]).addClass("filter-control-op").on("change.dropdown-select",function(m,l){j._triggerChange()});return this.$opSelect};e.prototype._renderValueInput=function c(){var j=this;if(this.currFilter.values){this.$valueSelect=dropDownSelect(this.currFilter.values,this.currFilter.values[0]).on("change.dropdown-select",function(l,k){j._triggerChange()})}else{this.$valueSelect=$("<input/>").addClass("form-control").on("change",function(k,l){j._triggerChange()})}this.$valueSelect.addClass("filter-control-value");return this.$valueSelect};e.prototype.val=function b(){var k=this.$element.find(".filter-control-key .dropdown-select-selected").text(),m=this.$element.find(".filter-control-op .dropdown-select-selected").text(),j=this.$element.find(".filter-control-value"),l=(j.hasClass("dropdown-select"))?(j.find(".dropdown-select-selected").text()):(j.val());return{key:k,op:m,value:l}};e.prototype._triggerChange=function h(){this.$element.trigger("change.filter-control",this.val())};jQuery.fn.extend({filterControl:function f(k){var j=jQuery.makeArray(arguments).slice(1);return this.map(function(){var n=$(this),m=n.data(e.prototype.DATA_KEY);if(jQuery.type(k)==="object"){m=new e(n,k);n.data(e.prototype.DATA_KEY,m)}if(m&&jQuery.type(k)==="string"){var l=m[k];if(jQuery.type(l)==="function"){return l.apply(m,j)}}return this})}})}()); \ No newline at end of file +var IconButton=Backbone.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:true,id:null,href:null,target:null,enabled:true,visible:true,tooltip_config:{}}});var IconButtonView=Backbone.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"};this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var a=this.template(this.model.toJSON());a.tooltip(this.model.get("tooltip_config"));this.$el.replaceWith(a);this.setElement(a);return this},events:{click:"click"},click:function(a){if(_.isFunction(this.model.get("on_click"))){this.model.get("on_click")(a);return false}return true},template:function(b){var a='title="'+b.title+'" class="icon-button';if(b.is_menu_button){a+=" menu-button"}a+=" "+b.icon_class;if(!b.enabled){a+="_disabled"}a+='"';if(b.id){a+=' id="'+b.id+'"'}a+=' href="'+b.href+'"';if(b.target){a+=' target="'+b.target+'"'}if(!b.visible){a+=' style="display: none;"'}if(b.enabled){a="<a "+a+"/>"}else{a="<span "+a+"/>"}return $(a)}});var IconButtonCollection=Backbone.Collection.extend({model:IconButton});var IconButtonMenuView=Backbone.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var a=this;this.collection.each(function(d){var b=$("<a/>").attr("href","javascript:void(0)").attr("title",d.attributes.title).addClass("icon-button menu-button").addClass(d.attributes.icon_class).appendTo(a.$el).click(d.attributes.on_click);if(d.attributes.tooltip_config){b.tooltip(d.attributes.tooltip_config)}var c=d.get("options");if(c){make_popupmenu(b,c)}});return this}});var create_icon_buttons_menu=function(b,a){if(!a){a={}}var c=new IconButtonCollection(_.map(b,function(d){return new IconButton(_.extend(d,a))}));return new IconButtonMenuView({collection:c})};var Grid=Backbone.Collection.extend({});var GridView=Backbone.View.extend({});var PopupMenu=Backbone.View.extend({initialize:function(b,a){this.$button=b;if(!this.$button.size()){this.$button=$("<div/>")}this.options=a||[];var c=this;this.$button.click(function(d){$(".popmenu-wrapper").remove();c._renderAndShow(d);return false})},_renderAndShow:function(a){this.render();this.$el.appendTo("body").css(this._getShownPosition(a)).show();this._setUpCloseBehavior()},render:function(){this.$el.addClass("popmenu-wrapper").hide().css({position:"absolute"}).html(this.template(this.$button.attr("id"),this.options));if(this.options.length){var a=this;this.$el.find("li").each(function(c,b){var d=a.options[c];if(d.func){$(this).children("a.popupmenu-option").click(function(e){d.func.call(a,e,d)})}})}return this},template:function(b,a){return['<ul id="',b,'-menu" class="dropdown-menu">',this._templateOptions(a),"</ul>"].join("")},_templateOptions:function(a){if(!a.length){return"<li>(no options)</li>"}return _.map(a,function(d){if(d.divider){return'<li class="divider"></li>'}else{if(d.header){return['<li class="head"><a href="javascript:void(0);">',d.html,"</a></li>"].join("")}}var c=d.href||"javascript:void(0);",e=(d.target)?(' target="'+d.target+'"'):(""),b=(d.checked)?('<span class="fa fa-check"></span>'):("");return['<li><a class="popupmenu-option" href="',c,'"',e,">",b,d.html,"</a></li>"].join("")}).join("")},_getShownPosition:function(b){var c=this.$el.width();var a=b.pageX-c/2;a=Math.min(a,$(document).scrollLeft()+$(window).width()-c-5);a=Math.max(a,$(document).scrollLeft()+5);return{top:b.pageY,left:a}},_setUpCloseBehavior:function(){var c=this;function a(e){$(document).off("click.close_popup");if(window.parent!==window){try{$(window.parent.document).off("click.close_popup")}catch(d){}}else{try{$("iframe#galaxy_main").contents().off("click.close_popup")}catch(d){}}c.remove()}$("html").one("click.close_popup",a);if(window.parent!==window){try{$(window.parent.document).find("html").one("click.close_popup",a)}catch(b){}}else{try{$("iframe#galaxy_main").contents().one("click.close_popup",a)}catch(b){}}},addItem:function(b,a){a=(a>=0)?a:this.options.length;this.options.splice(a,0,b);return this},removeItem:function(a){if(a>=0){this.options.splice(a,1)}return this},findIndexByHtml:function(b){for(var a=0;a<this.options.length;a++){if(_.has(this.options[a],"html")&&(this.options[a].html===b)){return a}}return null},findItemByHtml:function(a){return this.options[(this.findIndexByHtml(a))]},toString:function(){return"PopupMenu"}});PopupMenu.create=function _create(b,a){return new PopupMenu(b,a)};PopupMenu.make_popupmenu=function(b,c){var a=[];_.each(c,function(f,d){var e={html:d};if(f===null){e.header=true}else{if(jQuery.type(f)==="function"){e.func=f}}a.push(e)});return new PopupMenu($(b),a)};PopupMenu.convertLinksToOptions=function(c,a){c=$(c);a=a||"a";var b=[];c.find(a).each(function(g,e){var f={},d=$(g);f.html=d.text();if(d.attr("href")){var j=d.attr("href"),k=d.attr("target"),h=d.attr("confirm");f.func=function(){if((h)&&(!confirm(h))){return}switch(k){case"_parent":window.parent.location=j;break;case"_top":window.top.location=j;break;default:window.location=j}}}b.push(f)});return b};PopupMenu.fromExistingDom=function(d,c,a){d=$(d);c=$(c);var b=PopupMenu.convertLinksToOptions(c,a);c.remove();return new PopupMenu(d,b)};PopupMenu.make_popup_menus=function(c,b,d){c=c||document;b=b||"div[popupmenu]";d=d||function(e,f){return"#"+e.attr("popupmenu")};var a=[];$(c).find(b).each(function(){var e=$(this),f=$(c).find(d(e,c));a.push(PopupMenu.fromDom(f,e));f.addClass("popup")});return a};var faIconButton=function(a){a=a||{};a.tooltipConfig=a.tooltipConfig||{placement:"bottom"};a.classes=["icon-btn"].concat(a.classes||[]);if(a.disabled){a.classes.push("disabled")}var b=['<a class="',a.classes.join(" "),'"',((a.title)?(' title="'+a.title+'"'):("")),((!a.disabled&&a.target)?(' target="'+a.target+'"'):("")),' href="',((!a.disabled&&a.href)?(a.href):("javascript:void(0);")),'">','<span class="fa ',a.faIcon,'"></span>',"</a>"].join("");var c=$(b).tooltip(a.tooltipConfig);if(_.isFunction(a.onclick)){c.click(a.onclick)}return c};(function(){function a(j,p){var d=27,m=13,c=$(j),e=true,g={initialVal:"",name:"search",placeholder:"search",classes:"",onclear:function(){},onfirstsearch:null,onsearch:function(q){},minSearchLen:0,escWillClear:true,oninit:function(){}};function i(q){var r=$(this).parent().children("input");r.val("");r.trigger("clear:searchInput");p.onclear()}function o(r,q){$(this).trigger("search:searchInput",q);if(typeof p.onfirstsearch==="function"&&e){e=false;p.onfirstsearch(q)}else{p.onsearch(q)}}function f(){return['<input type="text" name="',p.name,'" placeholder="',p.placeholder,'" ','class="search-query ',p.classes,'" ',"/>"].join("")}function l(){return $(f()).focus(function(q){$(this).select()}).keyup(function(r){if(r.which===d&&p.escWillClear){i.call(this,r)}else{var q=$(this).val();if((r.which===m)||(p.minSearchLen&&q.length>=p.minSearchLen)){o.call(this,r,q)}else{if(!q.length){i.call(this,r)}}}}).val(p.initialVal)}function k(){return $(['<span class="search-clear fa fa-times-circle" ','title="',_l("clear search (esc)"),'"></span>'].join("")).tooltip({placement:"bottom"}).click(function(q){i.call(this,q)})}function n(){return $(['<span class="search-loading fa fa-spinner fa-spin" ','title="',_l("loading..."),'"></span>'].join("")).hide().tooltip({placement:"bottom"})}function h(){c.find(".search-loading").toggle();c.find(".search-clear").toggle()}if(jQuery.type(p)==="string"){if(p==="toggle-loading"){h()}return c}if(jQuery.type(p)==="object"){p=jQuery.extend(true,{},g,p)}return c.addClass("search-input").prepend([l(),k(),n()])}jQuery.fn.extend({searchInput:function b(c){return this.each(function(){return a(this,c)})}})}());(function(){function b(m,l){this.currModeIndex=0;return this.init(m,l)}b.prototype.DATA_KEY="mode-button";b.prototype.defaults={modes:[{mode:"default"}]};b.prototype.init=function f(m,l){l=l||{};this.$element=$(m);this.options=jQuery.extend(true,{},this.defaults,l);var o=this;this.$element.click(function n(p){o.callModeFn();o._incModeIndex();$(this).html(o.options.modes[o.currModeIndex].html)});this.currModeIndex=0;if(this.options.initialMode){this.currModeIndex=this._getModeIndex(this.options.initialMode)}return this};b.prototype._getModeIndex=function j(l){for(var m=0;m<this.options.modes.length;m+=1){if(this.options.modes[m].mode===l){return m}}throw new Error("mode not found: "+l)};b.prototype.getCurrMode=function a(){return this.options.modes[this.currModeIndex]};b.prototype.getMode=function g(l){if(!l){return this.getCurrMode()}return this.options.modes[(this._getModeIndex(l))]};b.prototype.hasMode=function k(l){return !!this.getMode(l)};b.prototype.currentMode=function e(){return this.options.modes[this.currModeIndex]};b.prototype.setMode=function c(m){var l=this.getMode(m);this.$element.html(l.html||null);return this};b.prototype._incModeIndex=function d(){this.currModeIndex+=1;if(this.currModeIndex>=this.options.modes.length){this.currModeIndex=0}return this};b.prototype.callModeFn=function h(l){var m=this.getMode(l).onclick;if(m&&jQuery.type(m==="function")){return m.call(this)}return undefined};jQuery.fn.extend({modeButton:function i(m){var l=jQuery.makeArray(arguments).slice(1);return this.map(function(){var p=$(this),o=p.data("mode-button");if(jQuery.type(m)==="object"){o=new b(p,m);p.data("mode-button",o)}else{if(o&&jQuery.type(m)==="string"){var n=o[m];if(jQuery.type(n)==="function"){return n.apply(o,l)}}else{if(o){return o}}}return this})}})}());function LoadingIndicator(a,c){var b=this;c=jQuery.extend({cover:false},c||{});function d(){var e=['<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 g=$(e).hide().css(c.css||{position:"fixed"}),f=g.children(".loading-indicator-text");if(c.cover){g.css({"z-index":2,top:a.css("top"),bottom:a.css("bottom"),left:a.css("left"),right:a.css("right"),opacity:0.5,"background-color":"white","text-align":"center"});f=g.children(".loading-indicator-text").css({"margin-top":"20px"})}else{f=g.children(".loading-indicator-text").css({margin:"12px 0px 0px 10px",opacity:"0.85",color:"grey"});f.children(".loading-indicator-message").css({margin:"0px 8px 0px 0px","font-style":"italic"})}return g}b.show=function(f,e,g){f=f||"loading...";e=e||"fast";b.$indicator=d().insertBefore(a);b.message(f);b.$indicator.fadeIn(e,g);return b};b.message=function(e){b.$indicator.find("i").text(e)};b.hide=function(e,f){e=e||"fast";if(b.$indicator&&b.$indicator.size()){b.$indicator.fadeOut(e,function(){b.$indicator.remove();if(f){f()}})}else{if(f){f()}}return b};return b}function dropDownSelect(b,c){c=c||((!_.isEmpty(b))?(b[0]):(""));var a=$(['<div class="dropdown-select btn-group">','<button type="button" class="btn btn-default">','<span class="dropdown-select-selected">'+c+"</span>","</button>","</div>"].join("\n"));if(b&&b.length>1){a.find("button").addClass("dropdown-toggle").attr("data-toggle","dropdown").append(' <span class="caret"></span>');a.append(['<ul class="dropdown-menu" role="menu">',_.map(b,function(e){return['<li><a href="javascript:void(0)">',e,"</a></li>"].join("")}).join("\n"),"</ul>"].join("\n"))}function d(g){var h=$(this),f=h.parents(".dropdown-select"),e=h.text();f.find(".dropdown-select-selected").text(e);f.trigger("change.dropdown-select",e)}a.find("a").click(d);return a}(function(){function e(k,j){return this.init(k,j)}e.prototype.DATA_KEY="filter-control";e.prototype.init=function g(k,j){j=j||{filters:[]};this.$element=$(k).addClass("filter-control btn-group");this.options=jQuery.extend(true,{},this.defaults,j);this.currFilter=this.options.filters[0];return this.render()};e.prototype.render=function d(){this.$element.empty().append([this._renderKeySelect(),this._renderOpSelect(),this._renderValueInput()]);return this};e.prototype._renderKeySelect=function a(){var j=this;var k=this.options.filters.map(function(l){return l.key});this.$keySelect=dropDownSelect(k,this.currFilter.key).addClass("filter-control-key").on("change.dropdown-select",function(m,l){j.currFilter=_.findWhere(j.options.filters,{key:l});j.render()._triggerChange()});return this.$keySelect};e.prototype._renderOpSelect=function i(){var j=this,k=this.currFilter.ops;this.$opSelect=dropDownSelect(k,k[0]).addClass("filter-control-op").on("change.dropdown-select",function(m,l){j._triggerChange()});return this.$opSelect};e.prototype._renderValueInput=function c(){var j=this;if(this.currFilter.values){this.$valueSelect=dropDownSelect(this.currFilter.values,this.currFilter.values[0]).on("change.dropdown-select",function(l,k){j._triggerChange()})}else{this.$valueSelect=$("<input/>").addClass("form-control").on("change",function(k,l){j._triggerChange()})}this.$valueSelect.addClass("filter-control-value");return this.$valueSelect};e.prototype.val=function b(){var k=this.$element.find(".filter-control-key .dropdown-select-selected").text(),m=this.$element.find(".filter-control-op .dropdown-select-selected").text(),j=this.$element.find(".filter-control-value"),l=(j.hasClass("dropdown-select"))?(j.find(".dropdown-select-selected").text()):(j.val());return{key:k,op:m,value:l}};e.prototype._triggerChange=function h(){this.$element.trigger("change.filter-control",this.val())};jQuery.fn.extend({filterControl:function f(k){var j=jQuery.makeArray(arguments).slice(1);return this.map(function(){var n=$(this),m=n.data(e.prototype.DATA_KEY);if(jQuery.type(k)==="object"){m=new e(n,k);n.data(e.prototype.DATA_KEY,m)}if(m&&jQuery.type(k)==="string"){var l=m[k];if(jQuery.type(l)==="function"){return l.apply(m,j)}}return this})}})}()); \ No newline at end of file diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 templates/webapps/galaxy/visualization/scatterplot.mako --- a/templates/webapps/galaxy/visualization/scatterplot.mako +++ b/templates/webapps/galaxy/visualization/scatterplot.mako @@ -216,7 +216,7 @@ )} ${h.js( - "mvc/visualizations/scatterplotControlForm", + "mvc/visualization/scatterplotControlForm", )} <script type="text/javascript"> diff -r 1afdcafa4b28e246efc0c96987c63e469e2255e7 -r 0d50efaac5d481b173f379a75b64a42b1b43b8a4 universe_wsgi.ini.sample --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -177,7 +177,7 @@ # Visualizations config directory: where to look for individual visualization plugins. # The path is relative to the Galaxy root dir. To use an absolute path begin the path # with '/'. -#visualizations_plugins_directory = config/plugins/visualizations +visualization_plugins_directory = config/plugins/visualizations # Each job is given a unique empty directory as its current working directory. # This option defines in what parent directory those directories will be 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.
participants (1)
-
commits-noreply@bitbucket.org