1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/42cbb2d2014c/ Changeset: 42cbb2d2014c User: carlfeberhard Date: 2013-08-07 21:43:44 Summary: Visualization framework: add plugin version of scatterplot to be managed in the repo Affected #: 14 files diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/Gruntfile.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/Gruntfile.js @@ -0,0 +1,56 @@ +// NOTE: use 'sudo npm install .', then 'grunt' to use this file + +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON( 'package.json' ), + + handlebars: { + compile: { + options: { + namespace: 'Templates', + processName : function( filepath ){ + return filepath.match( /\w*\.handlebars/ )[0].replace( '.handlebars', '' ); + } + }, + files: { + "build/compiled-templates.js" : "src/handlebars/*.handlebars" + } + } + }, + + concat: { + options: { + separator: ';\n' + }, + dist: { + //NOTE: mvc references templates - templates must be cat'd first + src : [ 'build/compiled-templates.js', 'src/**/*.js' ], + dest: 'build/scatterplot-concat.js' + } + }, + + uglify: { + options: { + }, + dist: { + src : 'build/scatterplot-concat.js', + // uglify directly into static dir + dest: 'static/scatterplot.js' + } + }, + + watch: { + files: [ 'src/**.js', 'src/handlebars/*.handlebars' ], + tasks: [ 'default' ] + } + }); + + grunt.loadNpmTasks( 'grunt-contrib-handlebars' ); + grunt.loadNpmTasks( 'grunt-contrib-concat' ); + grunt.loadNpmTasks( 'grunt-contrib-uglify' ); + grunt.loadNpmTasks( 'grunt-contrib-watch' ); + + grunt.registerTask( 'default', [ 'handlebars', 'concat', 'uglify' ]); + grunt.registerTask( 'watch', [ 'handlebars', 'concat', 'uglify', 'watch' ]); +}; diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/config/scatterplot.xml --- /dev/null +++ b/config/plugins/visualizations/scatterplot/config/scatterplot.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE visualization SYSTEM "../../visualization.dtd"> +<visualization name="scatterplot"> + <data_sources> + <data_source> + <model_class>HistoryDatasetAssociation</model_class> + <test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test> + <to_param param_attr="id">dataset_id</to_param> + </data_source> + </data_sources> + <params> + <param type="dataset" var_name_in_template="hda" required="true">dataset_id</param> + </params> + <template>scatterplot/templates/scatterplot.mako</template> +</visualization> diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/package.json --- /dev/null +++ b/config/plugins/visualizations/scatterplot/package.json @@ -0,0 +1,24 @@ +{ + "name": "galaxy-scatterplot", + "version": "0.0.0", + "description": "Scatterplot visualization plugin for the Galaxy informatics framework", + "main": " ", + "scripts": { + "test": "test" + }, + "keywords": [ + "galaxy", + "visualization", + "d3" + ], + "author": "Carl Eberhard", + "license": "BSD", + "devDependencies": { + "grunt": "~0.4.1", + "grunt-cli": "~0.1.9", + "grunt-contrib-handlebars": "~0.5.10", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-watch": "~0.5.1" + } +} diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/handlebars/chartControl.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/chartControl.handlebars @@ -0,0 +1,56 @@ +<p class="help-text"> + Use the following controls to how the chart is displayed. + The slide controls can be moved by the mouse or, if the 'handle' is in focus, your keyboard's arrow keys. + Move the focus between controls by using the tab or shift+tab keys on your keyboard. + Use the 'Draw' button to render (or re-render) the chart with the current settings. + </p> + + <div id="datapointSize" class="form-input numeric-slider-input"> + <label for="datapointSize">Size of data point: </label> + <div class="slider-output">{{datapointSize}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + Size of the graphic representation of each data point + </p> + </div> + + <div id="animDuration" class="form-input checkbox-input"> + <label for="animate-chart">Animate chart transitions?: </label> + <input type="checkbox" id="animate-chart" + class="checkbox control"{{#if animDuration}} checked="true"{{/if}} /> + <p class="form-help help-text-small"> + Uncheck this to disable the animations used on the chart + </p> + </div> + + <div id="width" class="form-input numeric-slider-input"> + <label for="width">Chart width: </label> + <div class="slider-output">{{width}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + (not including chart margins and axes) + </p> + </div> + + <div id="height" class="form-input numeric-slider-input"> + <label for="height">Chart height: </label> + <div class="slider-output">{{height}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + (not including chart margins and axes) + </p> + </div> + + <div id="X-axis-label"class="text-input form-input"> + <label for="X-axis-label">Re-label the X axis: </label> + <input type="text" name="X-axis-label" id="X-axis-label" value="{{xLabel}}" /> + <p class="form-help help-text-small"></p> + </div> + + <div id="Y-axis-label" class="text-input form-input"> + <label for="Y-axis-label">Re-label the Y axis: </label> + <input type="text" name="Y-axis-label" id="Y-axis-label" value="{{yLabel}}" /> + <p class="form-help help-text-small"></p> + </div> + + <input id="render-button" type="button" value="Draw" /> \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/handlebars/chartDisplay.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/chartDisplay.handlebars @@ -0,0 +1,1 @@ +<svg width="{{width}}" height="{{height}}"></svg> \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/handlebars/dataControl.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/dataControl.handlebars @@ -0,0 +1,56 @@ +<p class="help-text"> + Use the following controls to change the data used by the chart. + Use the 'Draw' button to render (or re-render) the chart with the current settings. + </p> + + {{! column selector containers }} + <div class="column-select"> + <label for="X-select">Data column for X: </label> + <select name="X" id="X-select"> + {{#each numericColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + <div class="column-select"> + <label for="Y-select">Data column for Y: </label> + <select name="Y" id="Y-select"> + {{#each numericColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + + {{! optional id column }} + <div id="include-id"> + <label for="include-id-checkbox">Include a third column as data point IDs?</label> + <input type="checkbox" name="include-id" id="include-id-checkbox" /> + <p class="help-text-small"> + These will be displayed (along with the x and y values) when you hover over + a data point. + </p> + </div> + <div class="column-select" style="display: none"> + <label for="ID-select">Data column for IDs: </label> + <select name="ID" id="ID-select"> + {{#each allColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + + {{! if we're using generic column selection names ('column 1') - allow the user to use the first line }} + <div id="first-line-header" style="display: none;"> + <p>Possible headers: {{ possibleHeaders }} + </p> + <label for="first-line-header-checkbox">Use the above as column headers?</label> + <input type="checkbox" name="include-id" id="first-line-header-checkbox" + {{#if usePossibleHeaders }}checked="true"{{/if}}/> + <p class="help-text-small"> + It looks like Galaxy couldn't get proper column headers for this data. + Would you like to use the column headers above as column names to select columns? + </p> + </div> + + <input id="render-button" type="button" value="Draw" /> + <div class="clear"></div> \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/handlebars/scatterplotControlForm.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/scatterplotControlForm.handlebars @@ -0,0 +1,46 @@ +{{! main layout }} + +<div class="scatterplot-container chart-container tabbable tabs-left"> + {{! tab buttons/headers using Bootstrap }} + <ul class="nav nav-tabs"> + {{! start with the data controls as the displayed tab }} + <li class="active"><a href="#data-control" data-toggle="tab" class="tooltip" + title="Use this tab to change which data are used">Data Controls</a></li> + <li><a href="#chart-control" data-toggle="tab" class="tooltip" + title="Use this tab to change how the chart is drawn">Chart Controls</a></li> + <li><a href="#stats-display" data-toggle="tab" class="tooltip" + title="This tab will display overall statistics for your data">Statistics</a></li> + <li><a href="#chart-display" data-toggle="tab" class="tooltip" + title="This tab will display the chart">Chart</a> + {{! loading indicator - initially hidden }} + <div id="loading-indicator" style="display: none;"> + <img class="loading-img" src="{{loadingIndicatorImagePath}}" /> + <span class="loading-message">{{message}}</span> + </div> + </li> + </ul> + + {{! data form, chart config form, stats, and chart all get their own tab }} + <div class="tab-content"> + {{! ---------------------------- tab for data settings form }} + <div id="data-control" class="tab-pane active"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for chart graphics control form }} + <div id="chart-control" class="tab-pane"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for data statistics }} + <div id="stats-display" class="tab-pane"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for actual chart }} + <div id="chart-display" class="tab-pane"> + {{! chart rendered separately }} + </div> + + </div>{{! end .tab-content }} +</div>{{! end .chart-control }} \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/handlebars/statsDisplay.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/statsDisplay.handlebars @@ -0,0 +1,8 @@ +<p class="help-text">By column:</p> + <table id="chart-stats-table"> + <thead><th></th><th>X</th><th>Y</th></thead> + {{#each stats}} + <tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr> + </tr> + {{/each}} + </table> \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/scatterplot.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js @@ -0,0 +1,488 @@ +/* ============================================================================= +todo: + 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 + +============================================================================= */ +/** + * Two Variable scatterplot visualization using d3 + * Uses semi transparent circles to show density of data in x, y grid + * usage : + * var plot = new TwoVarScatterplot({ containerSelector : 'div#my-plot', ... }) + * plot.render( xColumnData, yColumnData ); + * + * depends on: d3, underscore + */ +function TwoVarScatterplot( config ){ + var TICK_LINE_AND_PADDING = 10, + GUESS_AT_SVG_CHAR_WIDTH = 7, + GUESS_AT_SVG_CHAR_HEIGHT = 10, + PADDING = 8, + X_LABEL_TOO_LONG_AT = 5; + + // set up logging + //this.debugging = true; + this.log = function(){ + if( this.debugging && console && console.debug ){ + var args = Array.prototype.slice.call( arguments ); + args.unshift( this.toString() ); + console.debug.apply( console, args ); + } + }; + this.log( 'new TwoVarScatterplot:', config ); + + // ........................................................ set up chart config + // config will default to these values when not passed in + //NOTE: called on new + this.defaults = { + id : 'TwoVarScatterplot', + containerSelector : 'body', + //TODO??: needed? + maxDataPoints : 30000, + datapointSize : 4, + animDuration : 500, + //TODO: variable effect (not always exactly # of ticks set to) + xNumTicks : 10, + yNumTicks : 10, + xAxisLabelBumpY : 40, + yAxisLabelBumpX : -40, + width : 400, + height : 400, + //TODO: anyway to make this a sub-obj? + marginTop : 50, + marginRight : 50, + marginBottom : 50, + marginLeft : 50, + + xMin : null, + xMax : null, + yMin : null, + yMax : null, + + xLabel : "X", + yLabel : "Y" + }; + this.config = _.extend( {}, this.defaults, config ); + this.log( 'intial config:', this.config ); + + this.updateConfig = function( newConfig, rerender ){ + // setter for chart config + //TODO: validate here + _.extend( this.config, newConfig ); + this.log( this + '.updateConfig:', this.config ); + //TODO: implement rerender flag + }; + + // ........................................................ helpers + this.toString = function(){ + return this.config.id; + }; + // conv. methods for svg transforms + this.translateStr = function( x, y ){ + return 'translate(' + x + ',' + y + ')'; + }; + this.rotateStr = function( d, x, y ){ + return 'rotate(' + d + ',' + x + ',' + y + ')'; + }; + + // ........................................................ initial element creation + this.adjustChartDimensions = function( top, right, bottom, left ){ + //this.log( this + '.adjustChartDimensions', arguments ); + top = top || 0; + right = right || 0; + bottom = bottom || 0; + left = left || 0; + this.svg + .attr( "width", this.config.width + ( this.config.marginRight + right ) + + ( this.config.marginLeft + left ) ) + .attr( "height", this.config.height + ( this.config.marginTop + top ) + + ( this.config.marginBottom + bottom ) ) + // initial is hidden - show it + .style( 'display', 'block' ); + + // move content group away from margins + //TODO: allow top, right axis + this.content = this.svg.select( "g.content" ) + .attr( "transform", this.translateStr( this.config.marginLeft + left, this.config.marginTop + top ) ); + }; + + // ........................................................ data and scales + this.preprocessData = function( data, min, max ){ + //this.log( this + '.preprocessData', arguments ); + //TODO: filter by min, max if set + + // set a cap on the data, limit to first n points + return ( data.length > this.config.maxDataPoints )? ( data.slice( 0, this.config.maxDataPoints ) ): ( data ); + }; + + this.findMinMaxes = function( xCol, yCol, meta ){ + //this.log( this + '.findMinMaxes', arguments ); + // configuration takes priority, otherwise meta (from the server) if passed, last-resort: compute it here + this.xMin = this.config.xMin || ( meta )?( meta[0].min ):( d3.min( xCol ) ); + this.xMax = this.config.xMax || ( meta )?( meta[0].max ):( d3.max( xCol ) ); + this.yMin = this.config.yMin || ( meta )?( meta[1].min ):( d3.min( yCol ) ); + this.yMax = this.config.yMax || ( meta )?( meta[1].max ):( d3.max( yCol ) ); + }; + + this.setUpScales = function(){ + //this.log( this + '.setUpScales', arguments ); + // Interpolation for x, y based on data domains + this.xScale = d3.scale.linear() + .domain([ this.xMin, this.xMax ]) + .range([ 0, this.config.width ]), + this.yScale = d3.scale.linear() + .domain([ this.yMin, this.yMax ]) + .range([ this.config.height, 0 ]); + }; + + // ........................................................ axis and ticks + this.setUpXAxis = function(){ + //this.log( this + '.setUpXAxis', arguments ); + // origin: bottom, left + //TODO: incoporate top, right + this.xAxisFn = d3.svg.axis() + .scale( this.xScale ) + .ticks( this.config.xNumTicks ) + .orient( 'bottom' ); + this.xAxis// = content.select( 'g#x-axis' ) + .attr( 'transform', this.translateStr( 0, this.config.height ) ) + .call( this.xAxisFn ); + //this.log( 'xAxis:', this.xAxis ); + + //TODO: adjust ticks when tick labels are long - move odds down and extend tick line + // (for now) hide them + var xLongestTickLabel = d3.max( _.map( [ this.xMin, this.xMax ], + function( number ){ return ( String( number ) ).length; } ) ); + //this.log( 'xLongestTickLabel:', xLongestTickLabel ); + if( xLongestTickLabel >= X_LABEL_TOO_LONG_AT ){ + this.xAxis.selectAll( 'g' ).filter( ':nth-child(odd)' ).style( 'display', 'none' ); + } + + this.log( 'this.config.xLabel:', this.config.xLabel ); + this.xAxisLabel// = xAxis.select( 'text#x-axis-label' ) + .attr( 'x', this.config.width / 2 ) + .attr( 'y', this.config.xAxisLabelBumpY ) + .attr( 'text-anchor', 'middle' ) + .text( this.config.xLabel ); + this.log( 'xAxisLabel:', this.xAxisLabel ); + }; + + this.setUpYAxis = function(){ + //this.log( this + '.setUpYAxis', arguments ); + this.yAxisFn = d3.svg.axis() + .scale( this.yScale ) + .ticks( this.config.yNumTicks ) + .orient( 'left' ); + this.yAxis// = content.select( 'g#y-axis' ) + .call( this.yAxisFn ); + //this.log( 'yAxis:', this.yAxis ); + + // a too complicated section for increasing the left margin when tick labels are long + // get the tick labels for the y axis + var yTickLabels = this.yAxis.selectAll( 'text' ).filter( function( e, i ){ return i !== 0; } ); + this.log( 'yTickLabels:', yTickLabels ); + + // get the longest label length (or 0 if no labels) + this.yLongestLabel = d3.max( + //NOTE: d3 returns an nested array - use the plain array inside ([0]) + yTickLabels[0].map( function( e, i ){ + return ( d3.select( e ).text() ).length; + }) + ) || 0; + //this.log( 'yLongestLabel:', this.yLongestLabel ); + //TODO: lose the guessing if possible + var neededY = TICK_LINE_AND_PADDING + ( this.yLongestLabel * GUESS_AT_SVG_CHAR_WIDTH ) + + PADDING + GUESS_AT_SVG_CHAR_HEIGHT; + //this.log( 'neededY:', neededY ); + + // increase width for yLongerStr, increase margin for y + //TODO??: (or transform each number: 2k) + this.config.yAxisLabelBumpX = -( neededY - GUESS_AT_SVG_CHAR_HEIGHT ); + if( this.config.marginLeft < neededY ){ + var adjusting = ( neededY ) - this.config.marginLeft; + adjusting = ( adjusting < 0 )?( 0 ):( adjusting ); + //this.log( 'adjusting:', adjusting ); + + // update dimensions, translations + this.adjustChartDimensions( 0, 0, 0, adjusting ); + } + //this.log( 'this.config.yAxisLableBumpx, this.config.marginLeft:', + // this.config.yAxisLabelBumpX, this.config.marginLeft ); + + this.yAxisLabel// = yAxis.select( 'text#y-axis-label' ) + .attr( 'x', this.config.yAxisLabelBumpX ) + .attr( 'y', this.config.height / 2 ) + .attr( 'text-anchor', 'middle' ) + .attr( 'transform', this.rotateStr( -90, this.config.yAxisLabelBumpX, this.config.height / 2 ) ) + .text( this.config.yLabel ); + //this.log( 'yAxisLabel:', this.yAxisLabel ); + }; + + // ........................................................ grid lines + this.renderGrid = function(){ + //this.log( this + '.renderGrid', arguments ); + // VERTICAL + // select existing + this.vGridLines = this.content.selectAll( 'line.v-grid-line' ) + .data( this.xScale.ticks( this.xAxisFn.ticks()[0] ) ); + + // append any extra lines needed (more ticks) + this.vGridLines.enter().append( 'svg:line' ) + .classed( 'grid-line v-grid-line', true ); + + // update the attributes of existing and appended + this.vGridLines + .attr( 'x1', this.xScale ) + .attr( 'y1', 0 ) + .attr( 'x2', this.xScale ) + .attr( 'y2', this.config.height ); + + // remove unneeded (less ticks) + this.vGridLines.exit().remove(); + //this.log( 'vGridLines:', this.vGridLines ); + + // HORIZONTAL + this.hGridLines = this.content.selectAll( 'line.h-grid-line' ) + .data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) ); + + this.hGridLines.enter().append( 'svg:line' ) + .classed( 'grid-line h-grid-line', true ); + + this.hGridLines + .attr( 'x1', 0 ) + .attr( 'y1', this.yScale ) + .attr( 'x2', this.config.width ) + .attr( 'y2', this.yScale ); + + this.hGridLines.exit().remove(); + //this.log( 'hGridLines:', this.hGridLines ); + }; + + // ........................................................ data points + this.renderDatapoints = function( xCol, yCol, ids ){ + this.log( this + '.renderDatapoints', arguments ); + var count = 0, + plot = this, + xPosFn = function( d, i ){ + //if( d ){ this.log( 'x.data:', newXCol[ i ], 'plotted:', plot.xScale( newXCol[ i ] ) ); } + return plot.xScale( xCol[ i ] ); + }, + yPosFn = function( d, i ){ + //if( d ){ this.log( 'y.data:', newYCol[ i ], 'plotted:', plot.yScale( newYCol[ i ] ) ); } + return plot.yScale( yCol[ i ] ); + }; + + //this.datapoints = this.addDatapoints( xCol, yCol, ids, ".glyph" ); + var datapoints = this.content.selectAll( '.glyph' ).data( xCol ); + + // enter - NEW data to be added as glyphs: give them a 'entry' position and style + count = 0; + datapoints.enter() + .append( 'svg:circle' ) + .each( function(){ count += 1; } ) + .classed( "glyph", true ) + .attr( "cx", 0 ) + .attr( "cy", this.config.height ) + // start all bubbles small... + .attr( "r", 0 ); + this.log( count, ' new glyphs created' ); + + // for all EXISTING glyphs and those that need to be added: transition anim to final state + count = 0; + datapoints + // ...animate to final position + .transition().duration( this.config.animDuration ) + .each( function(){ count += 1; } ) + .attr( "cx", xPosFn ) + .attr( "cy", yPosFn ) + .attr( "r", plot.config.datapointSize ); + this.log( count, ' existing glyphs transitioned' ); + + // events + // glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM + datapoints.exit() + .each( function(){ count += 1; } ) + .transition().duration( this.config.animDuration ) + .attr( "cy", this.config.height ) + .attr( "r", 0 ) + .remove(); + this.log( count, ' glyphs removed' ); + + this._addDatapointEventhandlers( datapoints, xCol, yCol, ids ); + }; + + this._addDatapointEventhandlers = function( datapoints, xCol, yCol, ids ){ + var plot = this; + datapoints + //TODO: remove magic numbers + .on( 'mouseover', function( d, i ){ + var datapoint = d3.select( this ); + datapoint + .style( 'fill', 'red' ) + .style( 'fill-opacity', 1 ); + + // create horiz, vert lines to axis + plot.content.append( 'line' ) + .attr( 'stroke', 'red' ) + .attr( 'stroke-width', 1 ) + // start not at center, but at the edge of the circle - to prevent mouseover thrashing + .attr( 'x1', datapoint.attr( 'cx' ) - plot.config.datapointSize ) + .attr( 'y1', datapoint.attr( 'cy' ) ) + .attr( 'x2', 0 ) + .attr( 'y2', datapoint.attr( 'cy' ) ) + .classed( 'hoverline', true ); + + // if the vertical hoverline + if( datapoint.attr( 'cy' ) < plot.config.height ){ + plot.content.append( 'line' ) + .attr( 'stroke', 'red' ) + .attr( 'stroke-width', 1 ) + .attr( 'x1', datapoint.attr( 'cx' ) ) + .attr( 'y1', datapoint.attr( 'cy' ) + plot.config.datapointSize ) + .attr( 'x2', datapoint.attr( 'cx' ) ) + .attr( 'y2', plot.config.height ) + .classed( 'hoverline', true ); + } + + var datapointWindowPos = $( this ).offset(); + plot.datapointInfoBox = plot.infoBox( + datapointWindowPos.top, datapointWindowPos.left, + plot.infoHtml( xCol[ i ], yCol[ i ], ( ids )?( ids[ i ] ):( undefined ) ) + ); + $( 'body' ).append( plot.datapointInfoBox ); + }) + .on( 'mouseout', function(){ + d3.select( this ) + .style( 'fill', 'black' ) + .style( 'fill-opacity', 0.2 ); + plot.content.selectAll( '.hoverline' ).remove(); + if( plot.datapointInfoBox ){ + plot.datapointInfoBox.remove(); + } + }); + }, + + this.render = function( columnData, meta ){ + this.log( this + '.render', arguments ); + this.log( '\t config:', this.config ); + + // prepare the data + //pre: columns passed are numeric + //pre: at least two columns are passed + //assume: first column is x, second column is y, any remaining aren't used + var xCol = columnData[0], + yCol = columnData[1], + ids = ( columnData.length > 2 )?( columnData[2] ):( undefined ); + //this.log( this + '.render', xCol.length, yCol.length, this.config ); + + //pre: xCol.len == yCol.len + xCol = this.preprocessData( xCol ); + yCol = this.preprocessData( yCol ); + this.log( 'xCol len', xCol.length, 'yCol len', yCol.length ); + + this.findMinMaxes( xCol, yCol, meta ); + //this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax ); + this.setUpScales(); + + // find (or build if it doesn't exist) the svg dom infrastructure + if( !this.svg ){ this.svg = d3.select( 'svg' ).attr( "class", "chart" ); } + if( !this.content ){ + this.content = this.svg.append( "svg:g" ).attr( "class", "content" ).attr( 'id', this.config.id ); + } + //this.log( 'svg:', this.svg ); + //this.log( 'content:', this.content ); + + this.adjustChartDimensions(); + + if( !this.xAxis ){ this.xAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' ); } + if( !this.xAxisLabel ){ + this.xAxisLabel = this.xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' ); + } + //this.log( 'xAxis:', this.xAxis, 'xAxisLabel:', this.xAxisLabel ); + + if( !this.yAxis ){ this.yAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' ); } + if( !this.yAxisLabel ){ + this.yAxisLabel = this.yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' ); + } + //this.log( 'yAxis:', this.yAxis, 'yAxisLabel:', this.yAxisLabel ); + + this.setUpXAxis(); + this.setUpYAxis(); + + this.renderGrid(); + this.renderDatapoints( xCol, yCol, ids ); + }; + + this.infoHtml = function( x, y, id ){ + var retDiv = $( '<div/>' ); + if( id ){ + $( '<div/>' ).text( id ).css( 'font-weight', 'bold' ).appendTo( retDiv ); + } + $( '<div/>' ).text( x ).appendTo( retDiv ); + $( '<div/>' ).text( y ).appendTo( retDiv ); + return retDiv.html(); + }; + + //TODO: html for now + this.infoBox = function( top, left, html, adjTop, adjLeft ){ + adjTop = adjTop || 0; + adjLeft = adjLeft || 20; + var infoBox = $( '<div />' ) + .addClass( 'chart-info-box' ) + .css({ + 'position' : 'absolute', + 'top' : top + adjTop, + 'left' : left + adjLeft + }); + infoBox.html( html ); + return infoBox; + }; + +} + +//============================================================================== diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/scatterplotControlForm.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/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 = BaseView.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 : '/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( '.tooltip' ).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 : Templates.scatterplotControlForm, + dataControl : Templates.dataControl, + chartControl : Templates.chartControl, + statsDisplay : Templates.statsDisplay, + chartDisplay : Templates.chartDisplay +}; + +//============================================================================== diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/src/visualization-templates.html --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/visualization-templates.html @@ -0,0 +1,182 @@ +<script type="text/template" class="template-visualization" id="template-visualization-scatterplotControlForm"> +{{! main layout }} + +<div class="scatterplot-container chart-container tabbable tabs-left"> + {{! tab buttons/headers using Bootstrap }} + <ul class="nav nav-tabs"> + {{! start with the data controls as the displayed tab }} + <li class="active"><a href="#data-control" data-toggle="tab" class="tooltip" + title="Use this tab to change which data are used">Data Controls</a></li> + <li><a href="#chart-control" data-toggle="tab" class="tooltip" + title="Use this tab to change how the chart is drawn">Chart Controls</a></li> + <li><a href="#stats-display" data-toggle="tab" class="tooltip" + title="This tab will display overall statistics for your data">Statistics</a></li> + <li><a href="#chart-display" data-toggle="tab" class="tooltip" + title="This tab will display the chart">Chart</a> + {{! loading indicator - initially hidden }} + <div id="loading-indicator" style="display: none;"> + <img class="loading-img" src="{{loadingIndicatorImagePath}}" /> + <span class="loading-message">{{message}}</span> + </div> + </li> + </ul> + + {{! data form, chart config form, stats, and chart all get their own tab }} + <div class="tab-content"> + {{! ---------------------------- tab for data settings form }} + <div id="data-control" class="tab-pane active"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for chart graphics control form }} + <div id="chart-control" class="tab-pane"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for data statistics }} + <div id="stats-display" class="tab-pane"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for actual chart }} + <div id="chart-display" class="tab-pane"> + {{! chart rendered separately }} + </div> + + </div>{{! end .tab-content }} +</div>{{! end .chart-control }} +</script> + +<script type="text/template" class="template-visualization" id="template-visualization-dataControl"> + + <p class="help-text"> + Use the following controls to change the data used by the chart. + Use the 'Draw' button to render (or re-render) the chart with the current settings. + </p> + + {{! column selector containers }} + <div class="column-select"> + <label for="X-select">Data column for X: </label> + <select name="X" id="X-select"> + {{#each numericColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + <div class="column-select"> + <label for="Y-select">Data column for Y: </label> + <select name="Y" id="Y-select"> + {{#each numericColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + + {{! optional id column }} + <div id="include-id"> + <label for="include-id-checkbox">Include a third column as data point IDs?</label> + <input type="checkbox" name="include-id" id="include-id-checkbox" /> + <p class="help-text-small"> + These will be displayed (along with the x and y values) when you hover over + a data point. + </p> + </div> + <div class="column-select" style="display: none"> + <label for="ID-select">Data column for IDs: </label> + <select name="ID" id="ID-select"> + {{#each allColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> + </div> + + {{! if we're using generic column selection names ('column 1') - allow the user to use the first line }} + <div id="first-line-header" style="display: none;"> + <p>Possible headers: {{ possibleHeaders }} + </p> + <label for="first-line-header-checkbox">Use the above as column headers?</label> + <input type="checkbox" name="include-id" id="first-line-header-checkbox" + {{#if usePossibleHeaders }}checked="true"{{/if}}/> + <p class="help-text-small"> + It looks like Galaxy couldn't get proper column headers for this data. + Would you like to use the column headers above as column names to select columns? + </p> + </div> + + <input id="render-button" type="button" value="Draw" /> + <div class="clear"></div> +</script> + +<script type="text/template" class="template-visualization" id="template-visualization-chartControl"> + <p class="help-text"> + Use the following controls to how the chart is displayed. + The slide controls can be moved by the mouse or, if the 'handle' is in focus, your keyboard's arrow keys. + Move the focus between controls by using the tab or shift+tab keys on your keyboard. + Use the 'Draw' button to render (or re-render) the chart with the current settings. + </p> + + <div id="datapointSize" class="form-input numeric-slider-input"> + <label for="datapointSize">Size of data point: </label> + <div class="slider-output">{{datapointSize}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + Size of the graphic representation of each data point + </p> + </div> + + <div id="animDuration" class="form-input checkbox-input"> + <label for="animate-chart">Animate chart transitions?: </label> + <input type="checkbox" id="animate-chart" + class="checkbox control"{{#if animDuration}} checked="true"{{/if}} /> + <p class="form-help help-text-small"> + Uncheck this to disable the animations used on the chart + </p> + </div> + + <div id="width" class="form-input numeric-slider-input"> + <label for="width">Chart width: </label> + <div class="slider-output">{{width}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + (not including chart margins and axes) + </p> + </div> + + <div id="height" class="form-input numeric-slider-input"> + <label for="height">Chart height: </label> + <div class="slider-output">{{height}}</div> + <div class="slider"></div> + <p class="form-help help-text-small"> + (not including chart margins and axes) + </p> + </div> + + <div id="X-axis-label"class="text-input form-input"> + <label for="X-axis-label">Re-label the X axis: </label> + <input type="text" name="X-axis-label" id="X-axis-label" value="{{xLabel}}" /> + <p class="form-help help-text-small"></p> + </div> + + <div id="Y-axis-label" class="text-input form-input"> + <label for="Y-axis-label">Re-label the Y axis: </label> + <input type="text" name="Y-axis-label" id="Y-axis-label" value="{{yLabel}}" /> + <p class="form-help help-text-small"></p> + </div> + + <input id="render-button" type="button" value="Draw" /> +</script> + +<script type="text/template" class="template-visualization" id="template-visualization-statsDisplay"> + <p class="help-text">By column:</p> + <table id="chart-stats-table"> + <thead><th></th><th>X</th><th>Y</th></thead> + {{#each stats}} + <tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr> + </tr> + {{/each}} + </table> +</script> + +<script type="text/template" class="template-visualization" id="template-visualization-chartDisplay"> + <svg width="{{width}}" height="{{height}}"></svg> +</script> diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/static/scatterplot.css --- /dev/null +++ b/config/plugins/visualizations/scatterplot/static/scatterplot.css @@ -0,0 +1,181 @@ +/*TODO: use/move into base.less*/ +* { margin: 0px; padding: 0px; } + +/* -------------------------------------------- general layout */ +div.tab-pane { + padding: 8px; +} + +/* -------------------------------------------- header */ +.header { + margin-bottom: 8px; +} + +#chart-header { + padding : 8px; + background-color: #ebd9b2; + margin-bottom: 16px; + overflow: auto; +} + +#chart-header .subtitle { + margin: -4px 0px 0px 4px; + padding : 0; + color: white; + font-size: small; +} + +/* -------------------------------------------- main layout */ +#scatterplot { + /*from width + margin of chart?*/ +} + +.scatterplot-container .tab-pane { +} + +/* -------------------------------------------- all controls */ + +#scatterplot input[type=button], +#scatterplot select { + width: 100%; + max-width: 256px; + margin-bottom: 8px; +} + +#scatterplot .help-text, +#scatterplot .help-text-small { + color: grey; +} + +#scatterplot .help-text { + padding-bottom: 16px; +} + +#scatterplot .help-text-small { + padding: 4px; + font-size: smaller; +} + +#scatterplot > * { +} + +#scatterplot input[value=Draw] { + display: block; + margin-top: 16px; +} + +#scatterplot .numeric-slider-input { + max-width: 70%; +} + +/* -------------------------------------------- data controls */ + +/* -------------------------------------------- chart controls */ +#chart-control .form-input { + /*display: table-row;*/ +} + +#chart-control label { + /*text-align: right;*/ + margin-bottom: 8px; + /*display: table-cell;*/ +} + +#chart-control .slider { + /*display: table-cell;*/ + height: 8px; + display: block; + margin: 8px 0px 0px 8px; +} + +#chart-control .slider-output { + /*display: table-cell;*/ + float: right; +} + +#chart-control input[type="text"] { + border: 1px solid lightgrey; +} + + +/* -------------------------------------------- statistics */ +#stats-display table#chart-stats-table { + width: 100%; +} + +#stats-display #chart-stats-table th { + width: 30%; + padding: 4px; + font-weight: bold; + color: grey; +} + +#stats-display #chart-stats-table td { + border: solid lightgrey; + border-width: 1px 0px 0px 1px; + padding: 4px; +} + +#stats-display #chart-stats-table td:nth-child(1) { + border-width: 1px 0px 0px 0px; + padding-right: 1em; + text-align: right; + font-weight: bold; + color: grey; +} + +/* -------------------------------------------- load indicators */ +#loading-indicator { + margin: 12px 0px 0px 8px; +} + +#scatterplot #loading-indicator .loading-message { + font-style: italic; + font-size: smaller; + color: grey; +} + +/* -------------------------------------------- chart area */ +#chart-holder { + overflow: auto; + margin-left: 8px; +} + +svg .grid-line { + fill: none; + stroke: lightgrey; + stroke-opacity: 0.5; + shape-rendering: crispEdges; + stroke-dasharray: 3, 3; +} + +svg .axis path, svg .axis line { + fill: none; + stroke: black; + shape-rendering: crispEdges; +} + +svg .axis text { + font-family: monospace; + font-size: 12px; +} + +svg #x-axis-label, svg #y-axis-label { + font-family: sans-serif; + font-size: 10px; +} + +svg .glyph { + stroke: none; + fill: black; + fill-opacity: 0.2; +} + +/* -------------------------------------------- info box */ +.chart-info-box { + border-radius: 4px; + padding: 4px; + background-color: white; + border: 1px solid black; +} + diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/static/scatterplot.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/static/scatterplot.js @@ -0,0 +1,1 @@ +function TwoVarScatterplot(a){var b=10,c=7,d=10,e=8,f=5;this.log=function(){if(this.debugging&&console&&console.debug){var a=Array.prototype.slice.call(arguments);a.unshift(this.toString()),console.debug.apply(console,a)}},this.log("new TwoVarScatterplot:",a),this.defaults={id:"TwoVarScatterplot",containerSelector:"body",maxDataPoints:3e4,datapointSize:4,animDuration:500,xNumTicks:10,yNumTicks:10,xAxisLabelBumpY:40,yAxisLabelBumpX:-40,width:400,height:400,marginTop:50,marginRight:50,marginBottom:50,marginLeft:50,xMin:null,xMax:null,yMin:null,yMax:null,xLabel:"X",yLabel:"Y"},this.config=_.extend({},this.defaults,a),this.log("intial config:",this.config),this.updateConfig=function(a){_.extend(this.config,a),this.log(this+".updateConfig:",this.config)},this.toString=function(){return this.config.id},this.translateStr=function(a,b){return"translate("+a+","+b+")"},this.rotateStr=function(a,b,c){return"rotate("+a+","+b+","+c+")"},this.adjustChartDimensions=function(a,b,c,d){a=a||0,b=b||0,c=c||0,d=d||0,this.svg.attr("width",this.config.width+(this.config.marginRight+b)+(this.config.marginLeft+d)).attr("height",this.config.height+(this.config.marginTop+a)+(this.config.marginBottom+c)).style("display","block"),this.content=this.svg.select("g.content").attr("transform",this.translateStr(this.config.marginLeft+d,this.config.marginTop+a))},this.preprocessData=function(a){return a.length>this.config.maxDataPoints?a.slice(0,this.config.maxDataPoints):a},this.findMinMaxes=function(a,b,c){this.xMin=this.config.xMin||c?c[0].min:d3.min(a),this.xMax=this.config.xMax||c?c[0].max:d3.max(a),this.yMin=this.config.yMin||c?c[1].min:d3.min(b),this.yMax=this.config.yMax||c?c[1].max:d3.max(b)},this.setUpScales=function(){this.xScale=d3.scale.linear().domain([this.xMin,this.xMax]).range([0,this.config.width]),this.yScale=d3.scale.linear().domain([this.yMin,this.yMax]).range([this.config.height,0])},this.setUpXAxis=function(){this.xAxisFn=d3.svg.axis().scale(this.xScale).ticks(this.config.xNumTicks).orient("bottom"),this.xAxis.attr("transform",this.translateStr(0,this.config.height)).call(this.xAxisFn);var a=d3.max(_.map([this.xMin,this.xMax],function(a){return String(a).length}));a>=f&&this.xAxis.selectAll("g").filter(":nth-child(odd)").style("display","none"),this.log("this.config.xLabel:",this.config.xLabel),this.xAxisLabel.attr("x",this.config.width/2).attr("y",this.config.xAxisLabelBumpY).attr("text-anchor","middle").text(this.config.xLabel),this.log("xAxisLabel:",this.xAxisLabel)},this.setUpYAxis=function(){this.yAxisFn=d3.svg.axis().scale(this.yScale).ticks(this.config.yNumTicks).orient("left"),this.yAxis.call(this.yAxisFn);var a=this.yAxis.selectAll("text").filter(function(a,b){return 0!==b});this.log("yTickLabels:",a),this.yLongestLabel=d3.max(a[0].map(function(a){return d3.select(a).text().length}))||0;var f=b+this.yLongestLabel*c+e+d;if(this.config.yAxisLabelBumpX=-(f-d),this.config.marginLeft<f){var g=f-this.config.marginLeft;g=0>g?0:g,this.adjustChartDimensions(0,0,0,g)}this.yAxisLabel.attr("x",this.config.yAxisLabelBumpX).attr("y",this.config.height/2).attr("text-anchor","middle").attr("transform",this.rotateStr(-90,this.config.yAxisLabelBumpX,this.config.height/2)).text(this.config.yLabel)},this.renderGrid=function(){this.vGridLines=this.content.selectAll("line.v-grid-line").data(this.xScale.ticks(this.xAxisFn.ticks()[0])),this.vGridLines.enter().append("svg:line").classed("grid-line v-grid-line",!0),this.vGridLines.attr("x1",this.xScale).attr("y1",0).attr("x2",this.xScale).attr("y2",this.config.height),this.vGridLines.exit().remove(),this.hGridLines=this.content.selectAll("line.h-grid-line").data(this.yScale.ticks(this.yAxisFn.ticks()[0])),this.hGridLines.enter().append("svg:line").classed("grid-line h-grid-line",!0),this.hGridLines.attr("x1",0).attr("y1",this.yScale).attr("x2",this.config.width).attr("y2",this.yScale),this.hGridLines.exit().remove()},this.renderDatapoints=function(a,b,c){this.log(this+".renderDatapoints",arguments);var d=0,e=this,f=function(b,c){return e.xScale(a[c])},g=function(a,c){return e.yScale(b[c])},h=this.content.selectAll(".glyph").data(a);d=0,h.enter().append("svg:circle").each(function(){d+=1}).classed("glyph",!0).attr("cx",0).attr("cy",this.config.height).attr("r",0),this.log(d," new glyphs created"),d=0,h.transition().duration(this.config.animDuration).each(function(){d+=1}).attr("cx",f).attr("cy",g).attr("r",e.config.datapointSize),this.log(d," existing glyphs transitioned"),h.exit().each(function(){d+=1}).transition().duration(this.config.animDuration).attr("cy",this.config.height).attr("r",0).remove(),this.log(d," glyphs removed"),this._addDatapointEventhandlers(h,a,b,c)},this._addDatapointEventhandlers=function(a,b,c,d){var e=this;a.on("mouseover",function(a,f){var g=d3.select(this);g.style("fill","red").style("fill-opacity",1),e.content.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",g.attr("cx")-e.config.datapointSize).attr("y1",g.attr("cy")).attr("x2",0).attr("y2",g.attr("cy")).classed("hoverline",!0),g.attr("cy")<e.config.height&&e.content.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",g.attr("cx")).attr("y1",g.attr("cy")+e.config.datapointSize).attr("x2",g.attr("cx")).attr("y2",e.config.height).classed("hoverline",!0);var h=$(this).offset();e.datapointInfoBox=e.infoBox(h.top,h.left,e.infoHtml(b[f],c[f],d?d[f]:void 0)),$("body").append(e.datapointInfoBox)}).on("mouseout",function(){d3.select(this).style("fill","black").style("fill-opacity",.2),e.content.selectAll(".hoverline").remove(),e.datapointInfoBox&&e.datapointInfoBox.remove()})},this.render=function(a,b){this.log(this+".render",arguments),this.log(" config:",this.config);var c=a[0],d=a[1],e=a.length>2?a[2]:void 0;c=this.preprocessData(c),d=this.preprocessData(d),this.log("xCol len",c.length,"yCol len",d.length),this.findMinMaxes(c,d,b),this.setUpScales(),this.svg||(this.svg=d3.select("svg").attr("class","chart")),this.content||(this.content=this.svg.append("svg:g").attr("class","content").attr("id",this.config.id)),this.adjustChartDimensions(),this.xAxis||(this.xAxis=this.content.append("g").attr("class","axis").attr("id","x-axis")),this.xAxisLabel||(this.xAxisLabel=this.xAxis.append("text").attr("class","axis-label").attr("id","x-axis-label")),this.yAxis||(this.yAxis=this.content.append("g").attr("class","axis").attr("id","y-axis")),this.yAxisLabel||(this.yAxisLabel=this.yAxis.append("text").attr("class","axis-label").attr("id","y-axis-label")),this.setUpXAxis(),this.setUpYAxis(),this.renderGrid(),this.renderDatapoints(c,d,e)},this.infoHtml=function(a,b,c){var d=$("<div/>");return c&&$("<div/>").text(c).css("font-weight","bold").appendTo(d),$("<div/>").text(a).appendTo(d),$("<div/>").text(b).appendTo(d),d.html()},this.infoBox=function(a,b,c,d,e){d=d||0,e=e||20;var f=$("<div />").addClass("chart-info-box").css({position:"absolute",top:a+d,left:b+e});return f.html(c),f}}this.Templates=this.Templates||{},this.Templates.chartControl=Handlebars.template(function(a,b,c,d,e){function f(){return' checked="true"'}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var g,h="",i="function",j=this.escapeExpression,k=this;return h+='<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 id="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(g=c.datapointSize)?g=g.call(b,{hash:{},data:e}):(g=b.datapointSize,g=typeof g===i?g.apply(b):g),h+=j(g)+'</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 id="animDuration" class="form-input checkbox-input">\n <label for="animate-chart">Animate chart transitions?: </label>\n <input type="checkbox" id="animate-chart"\n class="checkbox control"',g=c["if"].call(b,b.animDuration,{hash:{},inverse:k.noop,fn:k.program(1,f,e),data:e}),(g||0===g)&&(h+=g),h+=' />\n <p class="form-help help-text-small">\n Uncheck this to disable the animations used on the chart\n </p>\n </div>\n\n <div id="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(g=c.width)?g=g.call(b,{hash:{},data:e}):(g=b.width,g=typeof g===i?g.apply(b):g),h+=j(g)+'</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 id="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(g=c.height)?g=g.call(b,{hash:{},data:e}):(g=b.height,g=typeof g===i?g.apply(b):g),h+=j(g)+'</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 id="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="',(g=c.xLabel)?g=g.call(b,{hash:{},data:e}):(g=b.xLabel,g=typeof g===i?g.apply(b):g),h+=j(g)+'" />\n <p class="form-help help-text-small"></p>\n </div>\n\n <div id="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="',(g=c.yLabel)?g=g.call(b,{hash:{},data:e}):(g=b.yLabel,g=typeof g===i?g.apply(b):g),h+=j(g)+'" />\n <p class="form-help help-text-small"></p>\n </div>\n\n <input id="render-button" type="button" value="Draw" />'}),this.Templates.chartDisplay=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+='<svg width="',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'" height="',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'"></svg>'}),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 for="X-select">Data column for X: </label>\n <select name="X" id="X-select">\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 for="Y-select">Data column for Y: </label>\n <select name="Y" id="Y-select">\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="ID" id="ID-select">\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 <input id="render-button" type="button" value="Draw" />\n <div class="clear"></div>'}),this.Templates.scatterplotControlForm=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+='\n\n<div class="scatterplot-container chart-container tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active"><a href="#data-control" data-toggle="tab" class="tooltip"\n title="Use this tab to change which data are used">Data Controls</a></li>\n <li><a href="#chart-control" data-toggle="tab" class="tooltip"\n title="Use this tab to change how the chart is drawn">Chart Controls</a></li>\n <li><a href="#stats-display" data-toggle="tab" class="tooltip"\n title="This tab will display overall statistics for your data">Statistics</a></li>\n <li><a href="#chart-display" data-toggle="tab" class="tooltip"\n title="This tab will display the chart">Chart</a>\n \n <div id="loading-indicator" style="display: none;">\n <img class="loading-img" src="',(f=c.loadingIndicatorImagePath)?f=f.call(b,{hash:{},data:e}):(f=b.loadingIndicatorImagePath,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <span class="loading-message">',(f=c.message)?f=f.call(b,{hash:{},data:e}):(f=b.message,f=typeof f===h?f.apply(b):f),g+=i(f)+"</span>\n </div>\n </li>\n </ul>\n\n "+'\n <div class="tab-content">\n '+'\n <div id="data-control" class="tab-pane active">\n '+"\n </div>\n \n "+'\n <div id="chart-control" class="tab-pane">\n '+"\n </div>\n\n "+'\n <div id="stats-display" class="tab-pane">\n '+"\n </div>\n\n "+'\n <div id="chart-display" class="tab-pane">\n '+"\n </div>\n\n </div>"+"\n</div>"}),this.Templates.statsDisplay=Handlebars.template(function(a,b,c,d,e){function f(a,b){var d,e="";return e+="\n <tr><td>",(d=c.name)?d=d.call(a,{hash:{},data:b}):(d=a.name,d=typeof d===i?d.apply(a):d),e+=j(d)+"</td><td>",(d=c.xval)?d=d.call(a,{hash:{},data:b}):(d=a.xval,d=typeof d===i?d.apply(a):d),e+=j(d)+"</td><td>",(d=c.yval)?d=d.call(a,{hash:{},data:b}):(d=a.yval,d=typeof d===i?d.apply(a):d),e+=j(d)+"</td></tr>\n </tr>\n "}this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var g,h="",i="function",j=this.escapeExpression,k=this;return h+='<p class="help-text">By column:</p>\n <table id="chart-stats-table">\n <thead><th></th><th>X</th><th>Y</th></thead>\n ',g=c.each.call(b,b.stats,{hash:{},inverse:k.noop,fn:k.program(1,f,e),data:e}),(g||0===g)&&(h+=g),h+="\n </table>"});var ScatterplotControlForm=BaseView.extend(LoggableMixin).extend({className:"scatterplot-control-form",dataLoadDelay:4e3,dataLoadSize:5e3,loadingIndicatorImage:"loading_small_white_bg.gif",fetchMsg:"Fetching data...",renderMsg:"Rendering...",initialize:function(a){this.log(this+".initialize, attributes:",a),this.dataset=null,this.chartConfig=null,this.chart=null,this.loader=null,this.$dataControl=null,this.$chartControl=null,this.$statsDisplay=null,this.$chartDisplay=null,this.dataFetch=null,this.initializeFromAttributes(a),this.initializeChart(a),this.initializeDataLoader(a)},initializeFromAttributes:function(a){if(!a||!a.dataset)throw"ScatterplotView requires a dataset";if(this.dataset=a.dataset,"string"===jQuery.type(this.dataset.metadata_column_types)&&(this.dataset.metadata_column_types=this.dataset.metadata_column_types.split(", ")),this.log(" dataset:",this.dataset),this.dataset.comment_lines&&this.dataset.comment_lines.length){var b=this.dataset.comment_lines[0],c=b.split(" ");c.length===this.dataset.metadata_column_types.length&&(this.possibleHeaders=c)}if(!a.apiDatasetsURL)throw"ScatterplotView requires a apiDatasetsURL";this.dataURL=a.apiDatasetsURL+"/"+this.dataset.id+"?",this.log(" dataURL:",this.dataURL)},initializeChart:function(a){this.chartConfig=a.chartConfig||{},this.log(" initial chartConfig:",this.chartConfig),this.chart=new TwoVarScatterplot(this.chartConfig),this.chartConfig=this.chart.config},initializeDataLoader:function(a){var b=this;this.loader=new LazyDataLoader({url:null,start:a.start||0,total:a.total||this.dataset.metadata_data_lines,delay:this.dataLoadDelay,size:this.dataLoadSize,buildUrl:function(a,b){return this.url+"&"+jQuery.param({start_val:a,max_vals:b})}}),$(this.loader).bind("error",function(a,c,d){b.log("ERROR:",c,d),alert("ERROR fetching data:\n"+c+"\n"+d),b.hideLoadingIndicator()})},render:function(){return this.log(this+".render"),this.$el.append(ScatterplotControlForm.templates.mainLayout({loadingIndicatorImagePath:"/static/images/"+this.loadingIndicatorImage,message:""})),this.$dataControl=this._render_dataControl(),this.$chartControl=this._render_chartControl(),this.$statsDisplay=this.$el.find(".tab-pane#stats-display"),this.$chartDisplay=this._render_chartDisplay(),this.chartConfig.xColumn&&this.chartConfig.yColumn&&this.renderChart(),this.$el.find(".tooltip").tooltip(),this},_render_dataControl:function(){var a=this,b=[],c=[],d=this.possibleHeaders&&this.$dataControl?this.$dataControl.find("#first-line-header-checkbox").is(":checked"):!1;_.each(this.dataset.metadata_column_types,function(e,f){var g=f+1,h="column "+g;a.dataset.metadata_column_names?h=a.dataset.metadata_column_names[f]:d&&(h=a.possibleHeaders[f]),b.push({index:g,name:h}),("int"===e||"float"===e)&&c.push({index:g,name:h})});var e=this.$el.find(".tab-pane#data-control");return e.html(ScatterplotControlForm.templates.dataControl({allColumns:b,numericColumns:c,possibleHeaders:this.possibleHeaders?this.possibleHeaders.join(", "):"",usePossibleHeaders:d})),!this.dataset.metadata_column_names&&this.possibleHeaders&&e.find("#first-line-header").show(),e.find("#X-select").val(this.chartConfig.xColumn),e.find("#Y-select").val(this.chartConfig.yColumn),void 0!==this.chartConfig.idColumn&&(e.find("#include-id-checkbox").attr("checked",!0).trigger("change"),e.find("#ID-select").val(this.chartConfig.idColumn)),e},_render_chartControl:function(){var a=this,b=this.$el.find(".tab-pane#chart-control"),c={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};return b.append(ScatterplotControlForm.templates.chartControl(this.chartConfig)),b.find(".numeric-slider-input").each(function(){function b(){var a=$(this),b=a.slider("value");e.text(b)}var d=$(this),e=d.find(".slider-output"),f=d.find(".slider"),g=d.attr("id");f.slider(_.extend(c[g],{value:a.chartConfig[g],change:b,slide:b}))}),b},_render_chartDisplay:function(){var a=this.$el.find(".tab-pane#chart-display");return a.append(ScatterplotControlForm.templates.chartDisplay(this.chartConfig)),a},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(){this.$el.find('select[name="ID"]').parent().toggle()},rerenderDataControl:function(){this.$dataControl=this._render_dataControl()},showLoadingIndicator:function(a,b){a=a||"";var c=this.$el.find("div#loading-indicator");messageBox=c.find(".loading-message"),c.is(":visible")?a?messageBox.fadeOut("fast",function(){messageBox.text(a),messageBox.fadeIn("fast",b)}):b():(a&&messageBox.text(a),c.fadeIn("fast",b))},hideLoadingIndicator:function(a){this.$el.find("div#loading-indicator").fadeOut("fast",a)},renderChart:function(){this.log(this+".renderChart"),this.data=null,this.meta=null,_.extend(this.chartConfig,this.getChartSettings()),this.log(" chartConfig:",this.chartConfig),this.chart.updateConfig(this.chartConfig,!1),this.loader.url=this.dataURL+"&"+jQuery.param(this.getDataSettings()),this.log(" loader: total lines:",this.loader.total," url:",this.loader.url);var a=this;$(this.loader).bind("loaded.new",function(b,c){a.log(a+" loaded.new",c),a.postProcessDataFetchResponse(c),a.log(" postprocessed data:",a.data),a.log(" postprocessed meta:",a.meta),a.showLoadingIndicator(a.renderMsg,function(){a.chart.render(a.data,a.meta),a.renderStats(a.data,a.meta),a.hideLoadingIndicator()})}),$(this.loader).bind("complete",function(b,c){a.log(a+" complete",c),$(a.loader).unbind()}),a.showLoadingIndicator(a.fetchMsg,function(){a.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),a.loader.load()})},renderStats:function(){this.log(this+".renderStats"),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(){var a=this;newChartSettings=this.getChartSettings(),_.extend(this.chartConfig,newChartSettings),this.log("this.chartConfig:",this.chartConfig),this.chart.updateConfig(this.chartConfig,!1),a.data&&a.meta?a.showLoadingIndicator(a.renderMsg,function(){a.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),a.chart.render(a.data,a.meta),a.hideLoadingIndicator()}):this.renderChart()},postProcessDataFetchResponse:function(a){this.postProcessData(a.data),this.postProcessMeta(a.meta)},postProcessData:function(a){var b=this;b.data?_.each(a,function(a,c){b.data[c]=b.data[c].concat(a)}):b.data=a},postProcessMeta:function(a){var b=this,c=this.dataset.metadata_column_types;b.meta?_.each(a,function(a,d){var e=b.meta[d],f=c[d];if(e.count+=a.count?a.count:0,"int"===f||"float"===f){e.min=Math.min(a.min,e.min),e.max=Math.max(a.max,e.max),e.sum=a.sum+e.sum,e.mean=e.count?e.sum/e.count:null;var g=b.data[d].slice().sort(),h=Math.floor(g.length/2);e.median=0===g.length%2?(g[h]+g[h+1])/2:g[h]}}):b.meta=a},getDataSettings:function(){var a=this.getColumnSelections(),b=[];this.log(" columnSelections:",a),b=[a.X.colIndex-1,a.Y.colIndex-1],this.$dataControl.find("#include-id-checkbox").attr("checked")&&b.push(a.ID.colIndex-1);var c={data_type:"raw_data",provider:"column_with_stats",columns:"["+b+"]"};return this.log(" data settings (url params):",c),c},getColumnSelections:function(){var a={};return this.$dataControl.find("div.column-select select").each(function(){var b=$(this),c=b.val();a[b.attr("name")]={colIndex:c,colName:b.children('[value="'+c+'"]').text()}}),a},getChartSettings:function(){var a={},b=this.getColumnSelections();a.datapointSize=this.$chartControl.find("#datapointSize.numeric-slider-input").find(".slider").slider("value"),a.width=this.$chartControl.find("#width.numeric-slider-input").find(".slider").slider("value"),a.height=this.$chartControl.find("#height.numeric-slider-input").find(".slider").slider("value");var c=this.$chartControl.find("input#X-axis-label").val(),d=this.$chartControl.find("input#Y-axis-label").val();return a.xLabel="X"===c?b.X.colName:c,a.yLabel="Y"===d?b.Y.colName:d,a.animDuration=this.$chartControl.find("#animate-chart").is(":checked")?this.chart.defaults.animDuration:0,this.log(" chartSettings:",a),a},toString:function(){return"ScatterplotControlForm("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotControlForm.templates={mainLayout:Templates.scatterplotControlForm,dataControl:Templates.dataControl,chartControl:Templates.chartControl,statsDisplay:Templates.statsDisplay,chartDisplay:Templates.chartDisplay}; \ No newline at end of file diff -r c623f3d38221c588de3976c05b688387933cbf29 -r 42cbb2d2014cbdb4e227faefa69f0c9808d5b1e7 config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- /dev/null +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<title>${hda.name} | ${visualization_name}</title> + +## ---------------------------------------------------------------------------- +<link type="text/css" rel="Stylesheet" media="screen" href="/static/style/base.css"> +<link type="text/css" rel="Stylesheet" media="screen" href="/static/style/jquery-ui/smoothness/jquery-ui.css"> + +<link type="text/css" rel="Stylesheet" media="screen" href="/plugins/visualizations/scatterplot/static/scatterplot.css"> + +## ---------------------------------------------------------------------------- +<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/underscore.js"></script> +<script type="text/javascript" src="/static/scripts/libs/backbone/backbone.js"></script> +<script type="text/javascript" src="/static/scripts/libs/backbone/backbone-relational.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="/plugins/visualizations/scatterplot/static/scatterplot.js"></script> + +</head> + +## ---------------------------------------------------------------------------- +<body> +%if not embedded: +## dataset info: only show if on own page +<div id="chart-header" class="header"> + <h2 class="title">Scatterplot of '${hda.name}'</h2> + <p class="subtitle">${hda.info}</p> +</div> +%endif + +<div id="scatterplot" class="scatterplot-control-form"></div> + +<script type="text/javascript"> +$(function(){ + var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.get_api_value() ) )}, + querySettings = ${h.to_json_string( query_args )}, + chartConfig = _.extend( querySettings, { + containerSelector : '#chart', + //TODO: move to ScatterplotControlForm.initialize + marginTop : ( querySettings.marginTop > 20 )?( querySettings.marginTop ):( 20 ), + + xColumn : querySettings.xColumn, + yColumn : querySettings.yColumn, + idColumn : querySettings.idColumn + }); + //console.debug( querySettings ); + + var settingsForm = new ScatterplotControlForm({ + dataset : hda, + apiDatasetsURL : "${h.url_for( controller='/api/datasets', action='index' )}", + el : $( '#scatterplot' ), + chartConfig : chartConfig + }).render(); + +}); +</script> + +</body> 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.