diff --git a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js index 0429f1472bb..1f5c2f9595c 100644 --- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -20,7 +20,8 @@ var ScatterplotDisplay = Backbone.View.extend({ var view = this, config = this.model.get( 'config' ), //TODO: very tied to datasets - should be generalized eventually - xhr = jQuery.getJSON(((typeof parent.galaxy_config.root == 'undefined') ? '/' : parent.galaxy_config.root) + 'api/datasets/' + this.dataset.id, { + baseUrl = ( window.parent && parent.galaxy_config )? parent.galaxy_config.root : '/', + xhr = jQuery.getJSON( baseUrl + 'api/datasets/' + this.dataset.id, { data_type : 'raw_data', provider : 'dataset-column', limit : config.pagination.perPage, diff --git a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js index 5ef5bdc44fc..472c3a793d2 100644 --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -1 +1 @@ -function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(a,c){var d=d3.select(this).attr("cx"),e=d3.select(this).attr("cy");return 0>d||d>b.width?!0:0>e||e>b.height?!0:!1}).style("display","none")}function f(a,b,c){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['
',void 0!==b.idColumn?"
"+d[b.idColumn]+"
":"","
",j(d),"
","
",k(d),"
","
"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a,c){return a[b.xColumn]},k=function(a,c){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).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("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'

\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
\n \n
'+h((e=null!=(e=b.datapointSize||(null!=a?a.datapointSize:a))?e:g,typeof e===f?e.call(a,{name:"datapointSize",hash:{},data:d}):e))+'
\n
\n

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

\n
\n\n
\n \n
'+h((e=null!=(e=b.width||(null!=a?a.width:a))?e:g,typeof e===f?e.call(a,{name:"width",hash:{},data:d}):e))+'
\n
\n

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

\n
\n\n
\n \n
'+h((e=null!=(e=b.height||(null!=a?a.height:a))?e:g,typeof e===f?e.call(a,{name:"height",hash:{},data:d}):e))+'
\n
\n

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

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

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

\n
\n\n\n'},useData:!0}),this.scatterplot.datacontrol=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f,g="function",h=b.helperMissing,i='

\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n\n\n
\n
';return f=null!=(f=b.peek||(null!=a?a.peek:a))?f:h,e=typeof f===g?f.call(a,{name:"peek",hash:{},data:d}):f,null!=e&&(i+=e),i+'
\n
\n\n

\n Note: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n

\n\n\n'},useData:!0}),this.scatterplot.editor=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){return'
\n \n\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n'},useData:!0});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekColumnSelector({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-column-selector.change",function(a,c){b.model.set("config",c)}).on("peek-column-selector.rename",function(a,b){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON(("undefined"==typeof parent.galaxy_config.root?"/":parent.galaxy_config.root)+"api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['
','','loading...',"
"].join(""))},template:function(){var a=['
','
','

','','',"
",'
','
',"
","
","",'
'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(a){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a,b){var c=this.model.get("config"),d=this.$el.find(".stats-display"),e=c.xLabel,f=c.yLabel,g=$("").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].join(""))}));d.empty().append(g)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); +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(a,c){var d=d3.select(this).attr("cx"),e=d3.select(this).attr("cy");return 0>d||d>b.width?!0:0>e||e>b.height?!0:!1}).style("display","none")}function f(a,b,c){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['
',void 0!==b.idColumn?"
"+d[b.idColumn]+"
":"","
",j(d),"
","
",k(d),"
","
"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a,c){return a[b.xColumn]},k=function(a,c){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).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("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'

\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
\n \n
'+h((e=null!=(e=b.datapointSize||(null!=a?a.datapointSize:a))?e:g,typeof e===f?e.call(a,{name:"datapointSize",hash:{},data:d}):e))+'
\n
\n

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

\n
\n\n
\n \n
'+h((e=null!=(e=b.width||(null!=a?a.width:a))?e:g,typeof e===f?e.call(a,{name:"width",hash:{},data:d}):e))+'
\n
\n

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

\n
\n\n
\n \n
'+h((e=null!=(e=b.height||(null!=a?a.height:a))?e:g,typeof e===f?e.call(a,{name:"height",hash:{},data:d}):e))+'
\n
\n

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

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

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

\n
\n\n\n'},useData:!0}),this.scatterplot.datacontrol=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f,g="function",h=b.helperMissing,i='

\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n\n\n
\n
';return f=null!=(f=b.peek||(null!=a?a.peek:a))?f:h,e=typeof f===g?f.call(a,{name:"peek",hash:{},data:d}):f,null!=e&&(i+=e),i+'
\n
\n\n

\n Note: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n

\n\n\n'},useData:!0}),this.scatterplot.editor=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){return'
\n \n\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n'},useData:!0});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekColumnSelector({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-column-selector.change",function(a,c){b.model.set("config",c)}).on("peek-column-selector.rename",function(a,b){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=window.parent&&parent.galaxy_config?parent.galaxy_config.root:"/",d=jQuery.getJSON(c+"api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return d.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),d.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),d},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['
','','loading...',"
"].join(""))},template:function(){var a=['
','
','

','','',"
",'
','
',"
","
","",'
'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(a){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a,b){var c=this.model.get("config"),d=this.$el.find(".stats-display"),e=c.xLabel,f=c.yLabel,g=$("
",e,"",f,"
",b,"",a[0],"",a[1],"
").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].join(""))}));d.empty().append(g)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file diff --git a/lib/galaxy/visualization/plugins/plugin.py b/lib/galaxy/visualization/plugins/plugin.py index 2c549baed51..09b2769cc84 100644 --- a/lib/galaxy/visualization/plugins/plugin.py +++ b/lib/galaxy/visualization/plugins/plugin.py @@ -129,7 +129,7 @@ class VisualizationPlugin( pluginframework.Plugin, ServesStaticPluginMixin, Serv self.config = config base_url = context.get( 'base_url', '' ) - self.base_url = '/'.join([ base_url, self.name ]) + self.base_url = '/'.join([ base_url, self.name ]) if base_url else self.name self._set_up_static_plugin() @@ -139,87 +139,79 @@ class VisualizationPlugin( pluginframework.Plugin, ServesStaticPluginMixin, Serv self.resource_parser = resource_parser.ResourceParser( app ) - def render( self, trans=None, visualization=None, config=None, embedded=None, **kwargs ): + def render( self, trans=None, embedded=None, **kwargs ): """ - Return the text of the rendered plugin webpage/fragment. + Render and return the text of the non-saved plugin webpage/fragment. """ - if visualization: - return self._render_saved( visualization, config, trans=trans, embedded=embedded, **kwargs ) + config = {} + context = self._default_context_vars( embedded=embedded, **kwargs ) + return self._render( config, trans=trans, embedded=embedded, context=context, **kwargs ) - # get the config for passing to the template from the kwargs dict, parsed using the plugin's params setting - config_from_kwargs = self._query_dict_to_config( trans, kwargs ) - config = utils.OpenObject( **config_from_kwargs ) - - # further parse config to resources (models, etc.) used in template based on registry config - resources = self._query_dict_to_resources( trans, config ) - - # if a saved visualization, pass in the encoded visualization id or None if a new render - encoded_visualization_id = None - - visualization_display_name = self.config[ 'name' ] - title = kwargs.get( 'title', None ) - - return self._fill_template( trans, - - visualization_name=self.name, - visualization_display_name=visualization_display_name, - title=title, + def render_saved( self, visualization, config, trans=None, embedded=None, **kwargs ): + """ + Render and return the text of the plugin webpage/fragment using the + config/data of a saved visualization. + """ + context = self._default_context_vars( embedded=embedded, **kwargs ) + # update any values that were loaded from the Visualization + context.update( dict( + title=visualization.latest_revision.title, saved_visualization=visualization, - visualization_id=encoded_visualization_id, + visualization_id=trans.security.encode_id( visualization.id ), + )) + return self._render( config, trans=trans, embedded=embedded, context=context, **kwargs ) - embedded=embedded, + # ---- non-public + def _default_context_vars( self, **kwargs ): + """ + Meta variables passed to the template/renderer to describe the visualization + being rendered. + + These are the defaults used when a saved visualization isn't present to + provide them. + """ + return dict( + visualization_name=self.name, + visualization_display_name=self.config[ 'name' ], + title=kwargs.get( 'title', None ), + saved_visualization=None, + visualization_id=None, # NOTE: passing *unparsed* kwargs as query query=kwargs, - # NOTE: vars is a dictionary for shared data in the template - # this feels hacky to me but it's what mako recommends: - # http://docs.makotemplates.org/en/latest/runtime.html - vars={}, - config=config, - **resources ) - def _render_saved( self, visualization, config, trans=None, embedded=None, **kwargs ): + def _render( self, config, context, trans=None, embedded=None, **kwargs ): """ - Return the text of the saved visualization. + Build/fetch the variables needed to render the visualization and call the renderer. """ - config = config or {} + template_args = {} # get the config for passing to the template from the kwargs dict, parsed using the plugin's params setting + config = config or {} config_from_kwargs = self._query_dict_to_config( trans, kwargs ) config.update( config_from_kwargs ) config = utils.OpenObject( **config ) + template_args[ 'config' ] = config # further parse config to resources (models, etc.) used in template based on registry config resources = self._query_dict_to_resources( trans, config ) + template_args.update( resources ) - # if a saved visualization, pass in the encoded visualization id or None if a new render - encoded_visualization_id = trans.security.encode_id( visualization.id ) + # add any extra variables dealing with the visualization itself, saved visualizations, etc. + template_args.update( context ) - visualization_display_name = self.config[ 'name' ] - title = visualization.latest_revision.title + template_args.update( embedded=embedded ) + return self._fill_template( trans, **template_args ) - return self._fill_template( trans, - visualization_name=self.name, - visualization_display_name=visualization_display_name, - title=title, - saved_visualization=visualization, - visualization_id=encoded_visualization_id, - embedded=embedded, - # NOTE: passing *unparsed* kwargs as query - query=kwargs, - # NOTE: vars is a dictionary for shared data in the template - # this feels hacky to me but it's what mako recommends: - # http://docs.makotemplates.org/en/latest/runtime.html - vars={}, - config=config, - **resources - ) - - # ---------------- getting resources for visualization templates from link query strings def _fill_template( self, trans, **kwargs ): + # NOTE: (mako specific) vars is a dictionary for shared data in the template + # this feels hacky to me but it's what mako recommends: + # http://docs.makotemplates.org/en/latest/runtime.html + kwargs.update( vars={} ) template_filename = self.config[ 'entry_point' ][ 'file' ] return trans.fill_template( template_filename, template_lookup=self.template_lookup, **kwargs ) + # ---------------- getting resources for visualization templates from link query strings def _get_resource_params_and_modifiers( self ): """ Get params and modifiers for the given visualization as a 2-tuple. diff --git a/lib/galaxy/visualization/plugins/registry.py b/lib/galaxy/visualization/plugins/registry.py index 3153eeec5fd..897ad2af551 100644 --- a/lib/galaxy/visualization/plugins/registry.py +++ b/lib/galaxy/visualization/plugins/registry.py @@ -8,6 +8,7 @@ import os import weakref from galaxy.web import url_for +import galaxy.exceptions from galaxy.web.base import pluginframework from galaxy.visualization.plugins import config_parser @@ -114,6 +115,14 @@ class VisualizationsRegistry( pluginframework.PageServingPluginManager ): )) return plugin + def get_plugin( self, key ): + """ + Wrap to throw error if plugin not in registry. + """ + if key not in self.plugins: + raise galaxy.exceptions.ObjectNotFound( 'Unknown or invalid visualization: ' + key ) + return self.plugins[ key ] + # -- building links to visualizations from objects -- def get_visualizations( self, trans, target_object ): """ diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index 18a93f1b0f1..999d57f639d 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -716,38 +716,14 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio help="A description of the visualization; annotation is shown alongside published visualizations."), template="visualization/create.mako" ) - # - # Visualizations. - # + # ------------------------- registry. @web.expose @web.require_login( "use Galaxy visualizations", use_panels=True ) def saved( self, trans, id=None, revision=None, type=None, config=None, title=None, **kwargs ): """ """ - DEFAULT_VISUALIZATION_NAME = 'Unnamed Visualization' - - # post to saved in order to save a visualization - # TODO: re-route this one to clear up signature if trans.request.method == 'POST': - if type is None or config is None: - return HTTPBadRequest( 'A visualization type and config are required to save a visualization' ) - if isinstance( config, basestring ): - config = loads( config ) - title = title or DEFAULT_VISUALIZATION_NAME - - # TODO: allow saving to (updating) a specific revision - should be part of UsesVisualization - # TODO: would be easier if this returned the visualization directly - # check security if posting to existing visualization - if id is not None: - visualization = self.get_visualization( trans, id, check_ownership=True, check_accessible=False ) - # ??: on not owner: error raised, but not returned (status = 200) - - # TODO: there's no security check in save visualization (if passed an id) - returned = self.save_visualization( trans, config, type, id, title ) - - # redirect to GET to prevent annoying 'Do you want to post again?' dialog on page reload - render_url = web.url_for( controller='visualization', action='saved', id=returned.get( 'vis_id' ) ) - return trans.response.send_redirect( render_url ) + self._POST_to_saved( trans, id=id, revision=revision, type=type, config=config, title=title, **kwargs ) if id is None: return HTTPBadRequest( 'A valid visualization id is required to load a visualization' ) @@ -760,11 +736,15 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # re-add title to kwargs for passing to render if title: kwargs[ 'title' ] = title - return self.render( trans, visualization.type, visualization, config=config, **kwargs ) + plugin = self._get_plugin_from_registry( trans, visualization.type ) + try: + return plugin.render_saved( visualization, config, trans=trans, **kwargs ) + except Exception, exception: + self._handle_plugin_error( trans, visualization.type, exception ) @web.expose @web.require_login( "use Galaxy visualizations", use_panels=True ) - def render( self, trans, visualization_name, visualization=None, config=None, embedded=None, **kwargs ): + def render( self, trans, visualization_name, embedded=None, **kwargs ): """ Render the appropriate visualization template, parsing the `kwargs` into appropriate variables and resources (such as ORM models) @@ -772,32 +752,66 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio URL: /visualization/show/{visualization_name} """ - config = config or {} - - # validate name vs. registry - registry = trans.app.visualizations_registry - if not registry: - raise HTTPNotFound( 'No visualization registry (possibly disabled in galaxy.ini)' ) - if visualization_name not in registry.plugins: - raise HTTPNotFound( 'Unknown or invalid visualization: ' + visualization_name ) - plugin = registry.plugins[ visualization_name ] - - returned = None + plugin = self._get_plugin_from_registry( trans, visualization_name ) try: - returned = plugin.render( controller=self, trans=trans, - visualization=visualization, config=config, embedded=embedded, **kwargs ) - + return plugin.render( trans=trans, embedded=embedded, **kwargs ) except Exception, exception: - log.exception( 'error rendering visualization (%s): %s', visualization_name, str( exception ) ) - if trans.debug: - raise - returned = trans.show_error_message( - "There was an error rendering the visualization. " + - "Contact your Galaxy administrator if the problem persists." + - "
Details: " + str( exception ), use_panels=False ) + self._handle_plugin_error( trans, visualization_name, exception ) - return returned + def _POST_to_saved( self, trans, id=None, revision=None, type=None, config=None, title=None, **kwargs ): + """ + Save the visualiztion info (revision, type, config, title, etc.) to + the Visualization at `id` or to a new Visualization if `id` is None. + Uses POST/redirect/GET after a successful save, redirecting to GET. + """ + DEFAULT_VISUALIZATION_NAME = 'Unnamed Visualization' + + # post to saved in order to save a visualization + if type is None or config is None: + return HTTPBadRequest( 'A visualization type and config are required to save a visualization' ) + if isinstance( config, basestring ): + config = loads( config ) + title = title or DEFAULT_VISUALIZATION_NAME + + # TODO: allow saving to (updating) a specific revision - should be part of UsesVisualization + # TODO: would be easier if this returned the visualization directly + # check security if posting to existing visualization + if id is not None: + self.get_visualization( trans, id, check_ownership=True, check_accessible=False ) + # ??: on not owner: error raised, but not returned (status = 200) + # TODO: there's no security check in save visualization (if passed an id) + returned = self.save_visualization( trans, config, type, id, title ) + + # redirect to GET to prevent annoying 'Do you want to post again?' dialog on page reload + render_url = web.url_for( controller='visualization', action='saved', id=returned.get( 'vis_id' ) ) + return trans.response.send_redirect( render_url ) + + def _get_plugin_from_registry( self, trans, visualization_name ): + """ + Get the named plugin from the registry. + :raises HTTPNotFound: if registry has been turned off in config. + :raises HTTPNotFound: if visualization_name isn't a registered plugin. + """ + if not trans.app.visualizations_registry: + raise HTTPNotFound( 'No visualization registry (possibly disabled in galaxy.ini)' ) + return trans.app.visualizations_registry.get_plugin( visualization_name ) + + def _handle_plugin_error( self, trans, visualization_name, exception ): + """ + Log, raise if debugging; log and show html message if not. + """ + log.exception( 'error rendering visualization (%s): %s', visualization_name, str( exception ) ) + if trans.debug: + raise + return trans.show_error_message( + "There was an error rendering the visualization. " + + "Contact your Galaxy administrator if the problem persists." + + "
Details: " + str( exception ), use_panels=False ) + + # + # Visualizations. + # @web.expose @web.require_login() def trackster(self, trans, **kwargs): @@ -833,7 +847,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio app['viz_config'] = self.get_visualization_config( trans, vis ) # backup id - app['id'] = id; + app['id'] = id # add dataset id app['add_dataset'] = new_dataset_id diff --git a/test/unit/visualizations/test_VisualizationPlugin.py b/test/unit/visualizations/test_VisualizationPlugin.py new file mode 100644 index 00000000000..96e26b26066 --- /dev/null +++ b/test/unit/visualizations/test_VisualizationPlugin.py @@ -0,0 +1,112 @@ +""" +""" +import os +import imp +import unittest + +test_utils = imp.load_source( 'test_utils', + os.path.join( os.path.dirname( __file__), '..', 'unittest_utils', 'utility.py' ) ) +import galaxy_mock + +from galaxy.visualization.plugins import plugin as vis_plugin +from galaxy.visualization.plugins import resource_parser + +# ----------------------------------------------------------------------------- +glx_dir = test_utils.get_galaxy_root() +template_cache_dir = os.path.join( glx_dir, 'database', 'compiled_templates' ) +addtional_templates_dir = os.path.join( glx_dir, 'config', 'plugins', 'visualizations', 'common', 'templates' ) +vis_reg_path = 'config/plugins/visualizations' + +config1 = """\ + + + + + + HistoryDatasetAssociation + tabular.Tabular + dataset_id + + + + dataset_id + + + +""" + + +# ----------------------------------------------------------------------------- +class VisualizationsPlugin_TestCase( unittest.TestCase ): + + def test_default_init( self ): + """ + """ + vis_dir = galaxy_mock.MockDir({ + 'config' : { + 'vis1.xml' : '' + }, + 'static' : {}, + 'templates' : {}, + }) + config = dict() + plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path, + 'myvis', config ) + self.assertEqual( plugin.name, 'myvis' ) + self.assertEqual( plugin.path, vis_dir.root_path ) + self.assertEqual( plugin.config, {} ) + self.assertEqual( plugin.base_url, 'myvis' ) + # static + self.assertTrue( plugin.serves_static ) + self.assertEqual( plugin.static_path, vis_dir.root_path + '/static' ) + self.assertEqual( plugin.static_url, 'myvis/static' ) + # template + self.assertTrue( plugin.serves_templates ) + self.assertEqual( plugin.template_path, vis_dir.root_path + '/templates' ) + self.assertEqual( plugin.template_lookup.__class__.__name__, 'TemplateLookup' ) + # resource parser + self.assertIsInstance( plugin.resource_parser, resource_parser.ResourceParser ) + + def test_init_with_context( self ): + """ + """ + vis_dir = galaxy_mock.MockDir({ + 'config' : { + 'vis1.xml' : '' + }, + 'static' : {}, + 'templates' : {}, + }) + config = dict() + context = dict( + base_url='u/wot/m8', + template_cache_dir='template_cache', + additional_template_paths=[ 'one' ] + ) + plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path, + 'myvis', config, context=context ) + self.assertEqual( plugin.base_url, 'u/wot/m8/myvis' ) + # static + self.assertEqual( plugin.static_url, 'u/wot/m8/myvis/static' ) + # template + self.assertEqual( plugin.template_lookup.__class__.__name__, 'TemplateLookup' ) + + def test_init_without_static_or_templates( self ): + """ + """ + vis_dir = galaxy_mock.MockDir({ + 'config' : { + 'vis1.xml' : '' + } + }) + plugin = vis_plugin.VisualizationPlugin( galaxy_mock.MockApp(), vis_dir.root_path, + 'myvis', dict() ) + self.assertFalse( plugin.serves_static ) + self.assertFalse( plugin.serves_templates ) + # not sure what this would do, but... + +# ----------------------------------------------------------------------------- +# TODO: config parser tests (in separate file) + +if __name__ == '__main__': + unittest.main()
",e,"",f,"
",b,"",a[0],"",a[1],"