commit/galaxy-central: carlfeberhard: Popupmenu mvc: fix anchor target in renderOptions; Visualizations registry: default link target to galaxy_main; Scatterplot (in registry): general refactoring and cleanup, paginate data instead of loading all, allow zoom and pan; pack scripts
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/07193baab386/ Changeset: 07193baab386 User: carlfeberhard Date: 2013-11-08 17:02:23 Summary: Popupmenu mvc: fix anchor target in renderOptions; Visualizations registry: default link target to galaxy_main; Scatterplot (in registry): general refactoring and cleanup, paginate data instead of loading all, allow zoom and pan; pack scripts Affected #: 24 files diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/Gruntfile.js --- a/config/plugins/visualizations/scatterplot/Gruntfile.js +++ b/config/plugins/visualizations/scatterplot/Gruntfile.js @@ -6,6 +6,7 @@ pkg: grunt.file.readJSON( 'package.json' ), handlebars: { + // compile all hb templates into a single file in the build dir compile: { options: { namespace: 'Templates', @@ -20,6 +21,7 @@ }, concat: { + // concat the template file and any js files in the src dir into a single file in the build dir options: { separator: ';\n' }, @@ -31,16 +33,19 @@ }, uglify: { + // uglify the concat single file directly into the static dir options: { + //mangle : false, + //beautify : true }, dist: { src : 'build/scatterplot-concat.js', - // uglify directly into static dir - dest: 'static/scatterplot.js' + dest: 'static/scatterplot-edit.js' } }, watch: { + // watch for changes in the src dir files: [ 'src/**.js', 'src/handlebars/*.handlebars' ], tasks: [ 'default' ] } @@ -52,5 +57,6 @@ grunt.loadNpmTasks( 'grunt-contrib-watch' ); grunt.registerTask( 'default', [ 'handlebars', 'concat', 'uglify' ]); - grunt.registerTask( 'watch', [ 'handlebars', 'concat', 'uglify', 'watch' ]); + // you can run grunt watch directly: + // grunt watch }; diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/package.json --- a/config/plugins/visualizations/scatterplot/package.json +++ b/config/plugins/visualizations/scatterplot/package.json @@ -19,6 +19,6 @@ "grunt-contrib-handlebars": "~0.5.10", "grunt-contrib-concat": "~0.3.0", "grunt-contrib-uglify": "~0.2.2", - "grunt-contrib-watch": "~0.5.1" + "grunt-contrib-watch": "~0.5.3" } } diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/chartControl.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/chartControl.handlebars +++ /dev/null @@ -1,56 +0,0 @@ -<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 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/chartDisplay.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/chartDisplay.handlebars +++ /dev/null @@ -1,1 +0,0 @@ -<svg width="{{width}}" height="{{height}}"></svg> \ No newline at end of file diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/chartcontrol.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/chartcontrol.handlebars @@ -0,0 +1,47 @@ +<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 data-config-key="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 data-config-key="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 data-config-key="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 data-config-key="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="{{x.label}}" /> + <p class="form-help help-text-small"></p> +</div> + +<div data-config-key="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="{{y.label}}" /> + <p class="form-help help-text-small"></p> +</div> + +<button class="render-button btn btn-primary active">Draw</button> diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/dataControl.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/dataControl.handlebars +++ /dev/null @@ -1,56 +0,0 @@ -<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 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/datacontrol.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/datacontrol.handlebars @@ -0,0 +1,55 @@ +<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>Data column for X: </label> + <select name="xColumn"> + {{#each numericColumns}} + <option value="{{index}}">{{name}}</option> + {{/each}} + </select> +</div> +<div class="column-select"> + <label>Data column for Y: </label> + <select name="yColumn"> + {{#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="idColumn"> + {{#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> + +<button class="render-button btn btn-primary active">Draw</button> diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/editor.handlebars --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/handlebars/editor.handlebars @@ -0,0 +1,36 @@ +<div class="scatterplot-editor 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 title="Use this tab to change which data are used" + href="#data-control" data-toggle="tab">Data Controls</a> + </li> + <li> + <a title="Use this tab to change how the chart is drawn" + href="#chart-control" data-toggle="tab" >Chart Controls</a> + </li> + {{! both stats and chart start as disabled since there's no info yet }} + <li class="disabled"> + <a title="This tab will display the chart" + href="#chart-display" data-toggle="tab">Chart</a> + </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="scatterplot-config-control tab-pane active"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for chart graphics control form }} + <div id="chart-control" class="scatterplot-config-control tab-pane"> + {{! rendered separately }} + </div> + + {{! ---------------------------- tab for actual chart }} + <div id="chart-display" class="scatterplot-display tab-pane"></div> + + </div>{{! end .tab-content }} +</div>{{! end .chart-control }} diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/scatterplotControlForm.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/scatterplotControlForm.handlebars +++ /dev/null @@ -1,46 +0,0 @@ -{{! 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 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/handlebars/statsDisplay.handlebars --- a/config/plugins/visualizations/scatterplot/src/handlebars/statsDisplay.handlebars +++ /dev/null @@ -1,8 +0,0 @@ -<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 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js @@ -0,0 +1,237 @@ +/* ============================================================================= +todo: + Remove 'chart' names + Make this (the config control/editor) and the ScatterplotView (in scatterplot.js) both + views onto a visualization/revision model + Move margins into wid/hi calcs (so final svg dims are w/h) + Better separation of AJAX in scatterplot.js (maybe pass in function?) + Labels should auto fill in chart control when dataset has column_names + Allow column selection/config using the peek output as a base for UI + Allow setting perPage of config + Auto render if given data and/or config + Allow option to auto set width/height based on screen real estate avail. + Use d3.nest to allow grouping, pagination/filtration by group (e.g. chromCol) + Semantic HTML (figure, caption) + Save as visualization, load from visualization + Save as SVG/png + Does it work w/ Galaxy.Frame? + Embedding + Small multiples + Drag & Drop other splots onto current (redraw with new axis and differentiate the datasets) + + Subclass on specific datatypes? (vcf, cuffdiff, etc.) + What can be common/useful to other visualizations? + +============================================================================= */ +/** + * Scatterplot config control UI as a backbone view + * handles: + * configuring which data will be used + * configuring the plot display + */ +var ScatterplotConfigEditor = BaseView.extend( LoggableMixin ).extend({ + //TODO: !should be a view on a visualization model + //logger : console, + className : 'scatterplot-control-form', + + /** initialize requires a configuration Object containing a dataset Object */ + initialize : function( attributes ){ + //console.log( this + '.initialize, attributes:', attributes ); + if( !attributes || !attributes.config || !attributes.config.dataset ){ + throw new Error( "ScatterplotView requires a configuration and dataset" ); + } + this.dataset = attributes.config.dataset; + //console.log( 'dataset:', this.dataset ); + + this.plotView = new ScatterplotView({ + config : attributes.config + }); + }, + + // ------------------------------------------------------------------------- CONTROLS RENDERING + render : function(){ + //console.log( this + '.render' ); + + // render the tab controls, areas and loading indicator + this.$el.append( ScatterplotConfigEditor.templates.mainLayout({ + })); + + // render the tab content + this.$el.find( '#data-control' ).append( this._render_dataControl() ); + this._render_chartControls( this.$el.find( '#chart-control' ) ); + //this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' ); + this._render_chartDisplay(); + + //TODO: auto render if given both x, y column choices in query for page + + // set up behaviours + this.$el.find( '[title]' ).tooltip(); + + // uncomment any of the following to have that tab show on initial load (for testing) + //this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' ); + //this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + return this; + }, + + _render_dataControl : function(){ + // controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) + var dataset = this.dataset; + //console.log( 'metadata_column_types:', this.dataset.metadata_column_types ); + //console.log( 'metadata_column_names:', this.dataset.metadata_column_names ); + + var allColumns = _.map( dataset.metadata_column_types, function( type, i ){ + var column = { index: i, type: type, name: ( 'column ' + ( i + 1 ) ) }; + if( dataset.metadata_column_names && dataset.metadata_column_names[ i ] ){ + column.name = dataset.metadata_column_names[ i ]; + } + return column; + }); + var numericColumns = _.filter( allColumns, function( column, i ){ + return ( ( column.type === 'int' ) || ( column.type === 'float' ) ); + }); + if( numericColumns < 2 ){ + numericColumns = allColumns; + } + //console.log( 'allColumns:', allColumns ); + //console.log( 'numericColumns:', numericColumns ); + + // render the html + var $dataControl = this.$el.find( '.tab-pane#data-control' ); + $dataControl.html( ScatterplotConfigEditor.templates.dataControl({ + allColumns : allColumns, + numericColumns : numericColumns + })); + + // preset to column selectors if they were passed in the config in the query string + $dataControl.find( '[name="xColumn"]' ).val( this.plotView.config.xColumn || numericColumns[0].index ); + $dataControl.find( '[name="yColumn"]' ).val( this.plotView.config.yColumn || numericColumns[1].index ); + if( this.plotView.config.idColumn !== undefined ){ + $dataControl.find( '#include-id-checkbox' ).prop( 'checked', true ).trigger( 'change' ); + $dataControl.find( 'select[name="idColumn"]' ).val( this.plotView.config.idColumn ); + } + + return $dataControl; + }, + + _render_chartControls : function( $chartControls ){ + // tab content to control how the chart is rendered (data glyph size, chart size, etc.) + $chartControls.html( ScatterplotConfigEditor.templates.chartControl( this.plotView.config ) ); + //console.debug( '$chartControl:', $chartControls ); + + // set up behaviours, js on sliders + //console.debug( 'numeric sliders:', $chartControls.find( '.numeric-slider-input' ) ); + // what to do when the slider changes: update display and update chartConfig + var view = this, + // 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 } + }; + + function onSliderChange(){ + var $this = $( this ); + $this.siblings( '.slider-output' ).text( $this.slider( 'value' ) ); + } + $chartControls.find( '.numeric-slider-input' ).each( function(){ + var $this = $( this ), + configKey = $this.attr( 'data-config-key' ), + sliderSettings = _.extend( controlRanges[ configKey ], { + value : view.plotView.config[ configKey ], + change : onSliderChange, + slide : onSliderChange + }); + //console.debug( configKey + ' slider settings:', sliderSettings ); + $this.find( '.slider' ).slider( sliderSettings ); + }); +//TODO: to more common area (like render)? + // set label inputs to current x, y metadata_column_names (if any) + if( this.dataset.metadata_column_names ){ + //var colNames = this.dataset.metadata_column_names; + //$chartControls.find( 'input[name="X-axis-label"]' ).val( colNames ); + //$chartControls.find( 'input[name="Y-axis-label"]' ).val( colNames ); +//TODO: on change of x, y data controls + } + + //console.debug( '$chartControls:', $chartControls ); + return $chartControls; + }, + + _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' ); + this.plotView.setElement( $chartDisplay ); + this.plotView.render(); + return $chartDisplay; + }, + + // ------------------------------------------------------------------------- EVENTS + events : { + 'change #include-id-checkbox' : 'toggleThirdColumnSelector', + 'click #data-control .render-button' : 'renderChart', + 'click #chart-control .render-button' : 'renderChart' + }, + + toggleThirdColumnSelector : function(){ + // show/hide the id selector on the data settings panel + this.$el.find( 'select[name="idColumn"]' ).parent().toggle(); + }, + + // ------------------------------------------------------------------------- CHART/STATS RENDERING + renderChart : function(){ + //console.log( this + '.renderChart' ); + // fetch the data, (re-)render the chart + this.$el.find( '.nav li.disabled' ).removeClass( 'disabled' ); + this.updateConfigWithDataSettings(); + this.updateConfigWithChartSettings(); + this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' ); + this.plotView.fetchData(); + //console.debug( this.plotView.$el ); + }, + + // ------------------------------------------------------------------------- GET DATA/CHART SETTINGS + updateConfigWithDataSettings : function(){ + // parse the column values for both indeces (for the data fetch) and names (for the chart) + var $dataControls = this.$el.find( '#data-control' ); + var settings = { + xColumn : $dataControls.find( '[name="xColumn"]' ).val(), + yColumn : $dataControls.find( '[name="yColumn"]' ).val() + }; + if( $dataControls.find( '#include-id-checkbox' ).prop( 'checked' ) ){ + settings.idColumn = $dataControls.find( '[name="idColumn"]' ).val(); + } + //console.log( '\t data settings:', settings ); + return _.extend( this.plotView.config, settings ); + }, + + updateConfigWithChartSettings : function(){ + // gets the user-selected chartConfig from the chart settings panel + var plotView = this.plotView, + $chartControls = this.$el.find( '#chart-control' ); + // use a loop of config keys to get the form values for these sliders + [ 'datapointSize', 'width', 'height' ].forEach( function( v, i ){ + plotView.config[ v ] = $chartControls.find( '.numeric-slider-input[data-config-key="' + v + '"]' ) + .find( '.slider' ).slider( 'value' ); + }); + // update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName + plotView.config.x.label = $chartControls.find( 'input[name="X-axis-label"]' ).val(); + plotView.config.y.label = $chartControls.find( 'input[name="Y-axis-label"]' ).val(); + //console.log( '\t chartSettings:', settings ); + return plotView.config; + }, + + toString : function(){ + return 'ScatterplotConfigEditor(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')'; + } +}); + +ScatterplotConfigEditor.templates = { + mainLayout : Templates.editor, + dataControl : Templates.datacontrol, + chartControl : Templates.chartcontrol +}; + +//============================================================================== diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/scatterplot-display.js --- /dev/null +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -0,0 +1,288 @@ +// ============================================================================= +/** + * Scatterplot display control UI as a backbone view + * handles: + * fetching the data (if needed) + * computing and displaying data stats + * controls for pagination of data (if needed) + */ +var ScatterplotView = Backbone.View.extend({ + //TODO: should be a view on visualization(revision) model + + defaults : { + dataset : { + }, + metadata : { + dataLines : undefined + }, + + ajaxFn : null, + + pagination : { + currPage : 0, + perPage : 3000 + }, + + width : 400, + height : 400, + + margin : { + top : 16, + right : 16, + bottom : 40, + left : 54 + }, + + x : { + ticks : 10, + label : 'X' + }, + y : { + ticks : 10, + label : 'Y' + }, + + datapointSize : 4, + animDuration : 500 + }, + + initialize : function( attributes ){ + this.config = _.extend( _.clone( this.defaults ), attributes.config || {}); + //console.debug( this + '.config:', this.config ); + }, + + updateConfig : function( newConfig ){ + //console.log( this + '.updateConfig:', newConfig ); + this.config = this.config || {}; + //TODO: validate here + _.extend( this.config, newConfig ); + //TODO: implement rerender flag + }, + + fetchData : function(){ +//TODO: doesn't work bc it's rendered in render()... + this.showLoadingIndicator( 'getting data' ); + //console.debug( 'currPage', this.config.pagination.currPage ); + var view = this; +//TODO: very tied to datasets - should be generalized eventually + xhr = jQuery.getJSON( '/api/datasets/' + this.config.dataset.id, { + data_type : 'raw_data', + provider : 'dataset-column', + limit : this.config.pagination.perPage, + offset : ( this.config.pagination.currPage * this.config.pagination.perPage ) + }); + xhr.done( function( data ){ + view.renderData( data.data ); + }); + xhr.fail( function( xhr, status, message ){ + alert( 'Error loading data:\n' + xhr.responseText ); + console.error( xhr, status, message ); + }); + xhr.always( function(){ + view.hideLoadingIndicator(); + }); + return xhr; + }, + + render : function( data ){ + this.$el.addClass( 'scatterplot-display' ).html([ + '<div class="controls clear"></div>', + '<div class="loading-indicator">', + '<span class="fa fa-spinner fa-spin"></span>', + '<span class="loading-indicator-message"></span>', + '</div>', + '<svg/>', //TODO: id + '<div class="stats-display"></div>' + ].join( '' )); + this.$el.children().hide(); + + if( data ){ + this.renderData( data ); + } + return this; + }, + + showLoadingIndicator : function( message, speed ){ + // display the loading indicator over the tab panels if hidden, update message (if passed) +//TODO: move loading indicator into data-info-text + message = message || ''; + speed = speed || 'fast'; + var $indicator = this.$el.find( '.loading-indicator' ); + + if( message ){ $indicator.find( '.loading-indicator-message' ).text( message ); } + if( !$indicator.is( ':visible' ) ){ + this.toggleStats( false ); + $indicator.css({ left: ( this.config.width / 2 ), top: this.config.height / 2 }).show(); + } + }, + + hideLoadingIndicator : function( speed ){ + speed = speed || 'fast'; + this.$el.find( '.loading-indicator' ).hide(); + }, + + renderData : function( data ){ + this.$el.find( '.controls' ).empty().append( this.renderControls( data ) ).show(); + this.renderPlot( data ); + this.getStats( data ); + }, + + renderControls : function( data ){ + var view = this; + var $left = $( '<div class="left"></div>' ), + $right = $( '<div class="right"></div>' ); + + $left.append([ + this.renderPrevNext( data ), + this.renderPagination( data ) + ]); + $right.append([ + this.renderLineInfo( data ), + $( '<button>Stats</button>' ).addClass( 'stats-toggle-btn' ) + .click( function(){ + view.toggleStats(); + }), + $( '<button>Redraw</button>' ).addClass( 'rerender-btn' ) + .click( function(){ + view.renderPlot( data ); + }) + ]); + return [ $left, $right ]; + }, + + renderLineInfo : function( data ){ + var totalLines = this.config.dataset.metadata_data_lines || 'an unknown number of', + lineStart = ( this.config.pagination.currPage * this.config.pagination.perPage ), + lineEnd = lineStart + data.length; + return $( '<p/>' ).addClass( 'scatterplot-data-info' ) + .text([ 'Displaying lines', lineStart + 1, 'to', lineEnd, 'of', totalLines, 'lines' ].join( ' ' )); + }, + + renderPrevNext : function( data ){ + // this is cra-zazy + if( !data + || ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; } + + function makePage$Li( text ){ + return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' )); + } +//TODO: cache numPages/numLines in config + var view = this, + dataLines = this.config.dataset.metadata_data_lines, + numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined ); + //console.debug( 'data:', this.config.dataset.metadata_data_lines, 'numPages:', numPages ); + + // prev next buttons + var $prev = makePage$Li( 'Prev' ).click( function(){ + if( view.config.pagination.currPage > 0 ){ + view.config.pagination.currPage -= 1; + view.fetchData(); + } + }), + $next = makePage$Li( 'Next' ).click( function(){ + if( !numPages || view.config.pagination.currPage < ( numPages - 1 ) ){ + view.config.pagination.currPage += 1; + view.fetchData(); + } + }), + $prevNextList = $( '<ul/>' ).addClass( 'pagination data-prev-next' ) + .append([ $prev, $next ]); + + if( view.config.pagination.currPage === 0 ){ + $prev.addClass( 'disabled' ); + } + if( numPages && view.config.pagination.currPage === ( numPages - 1 ) ){ + $next.addClass( 'disabled' ); + } + return $prevNextList; + }, + + renderPagination : function( data ){ + // this is cra-zazy + if( !data + || ( this.config.pagination.currPage === 0 && data.length < this.config.pagination.perPage ) ){ return null; } + + function makePage$Li( text ){ + return $([ '<li><a href="javascript:void(0);">', text, '</a></li>' ].join( '' )); + } +//TODO: cache numPages/numLines in config + var view = this, + dataLines = this.config.dataset.metadata_data_lines, + numPages = ( dataLines )?( Math.ceil( dataLines / this.config.pagination.perPage ) ):( undefined ); + //console.debug( 'data:', this.config.dataset.metadata_data_lines, 'numPages:', numPages ); + + // page numbers (as separate control) + //var $paginationContainer = $( '<div/>' ).addClass( 'pagination-container' ), + var $pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' ); + function pageNumClick( ev ){ + view.config.pagination.currPage = $( this ).data( 'page' ); + view.fetchData(); + } + for( var i=0; i<numPages; i+=1 ){ + // add page data for later event handling + var $pageLi = makePage$Li( i + 1 ).attr( 'data-page', i ).click( pageNumClick ); + if( i === this.config.pagination.currPage ){ + $pageLi.addClass( 'active' ); + } + $pagesList.append( $pageLi ); + } + // placing the pages list in an extra container allows us to set a max-width and scroll if overflow + //$paginationContainer.append( $pagesList ); + //return $paginationContainer; + return $pagesList; + }, + + renderPlot : function( data ){ + this.toggleStats( false ); + var $svg = this.$el.find( 'svg' ); + $svg.off().empty().show(); + scatterplot( $svg.get( 0 ), this.config, data ); + }, + + getStats : function( data ){ + var view = this; + meanWorker = new Worker( '/plugins/visualizations/scatterplot/static/worker-stats.js' ); + meanWorker.postMessage({ + data : data, + keys : [ this.config.xColumn, this.config.yColumn ] + }); + meanWorker.onerror = function( event ){ + meanWorker.terminate(); + }; + meanWorker.onmessage = function( event ){ + view.renderStats( event.data ); + }; + }, + + renderStats : function( stats, error ){ + //console.debug( 'renderStats:', stats, error ); + //console.debug( JSON.stringify( stats, null, ' ' ) ); + var $statsTable = this.$el.find( '.stats-display' ); + + var xLabel = this.config.x.label, yLabel = this.config.y.label, + $table = $( '<table/>' ).addClass( 'table' ) + .append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' )) + .append( _.map( stats, function( stat, key ){ + return $([ '<tr><td>', key, '</td><td>', stat[0], '</td><td>', stat[1], '</td></tr>' ].join( '' )); + })); + $statsTable.empty().append( $table ); + }, + + toggleStats : function( showStats ){ + var $statsDisplay = this.$el.find( '.stats-display' ); + showStats = ( showStats === undefined )?( $statsDisplay.is( ':hidden' ) ):( showStats ); + if( showStats ){ + this.$el.find( 'svg' ).hide(); + $statsDisplay.show(); + this.$el.find( '.controls .stats-toggle-btn' ).text( 'Plot' ); + } else { + $statsDisplay.hide(); + this.$el.find( 'svg' ).show(); + this.$el.find( '.controls .stats-toggle-btn' ).text( 'Stats' ); + } + }, + + toString : function(){ + return 'ScatterplotView()'; + } +}); diff -r 0780dcf3158cb10e69b82924820823c919797410 -r 07193baab386447cb08eea42460dd4c544097c36 config/plugins/visualizations/scatterplot/src/scatterplot.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot.js @@ -1,488 +1,278 @@ -/* ============================================================================= -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 + * var plot = new scatterplot( $( 'svg' ).get(0), config, data ) */ -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 + ')'; +function scatterplot( renderTo, config, data ){ + //console.log( 'scatterplot', config ); + + var translateStr = function( x, y ){ + return 'translate(' + x + ',' + y + ')'; + }, + rotateStr = function( d, x, y ){ + return 'rotate(' + d + ',' + x + ',' + y + ')'; + }, + getX = function( d, i ){ + //console.debug( d[ config.xColumn ] ); + return d[ config.xColumn ]; + }, + getY = function( d, i ){ + //console.debug( d[ config.yColumn ] ); + return d[ config.yColumn ]; + }; + + // .................................................................... scales + var stats = { + x : { extent: d3.extent( data, getX ) }, + y : { extent: d3.extent( data, getY ) } + }; + + //TODO: set pan/zoom limits + // from http://stackoverflow.com/questions/10422738/limiting-domain-when-zooming-or-... + //self.x.domain([Math.max(self.x.domain()[0], self.options.xmin), Math.min(self.x.domain()[1], self.options.xmax)]); + //self.y.domain([Math.max(self.y.domain()[0], self.options.ymin), Math.min(self.y.domain()[1], self.options.ymax)]); + var interpolaterFns = { + x : d3.scale.linear() + .domain( stats.x.extent ) + .range([ 0, config.width ]), + y : d3.scale.linear() + .domain( stats.y.extent ) + .range([ config.height, 0 ]) }; - // ........................................................ 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 ); + // .................................................................... main components + var zoom = d3.behavior.zoom() + .x( interpolaterFns.x ) + .y( interpolaterFns.y ) + .scaleExtent([ 1, 10 ]); +//TODO: you can prog. set the zoom and pan with zoom.scale( val ) and zoom.translate([ x, y ])... + + //console.debug( renderTo ); + var svg = d3.select( renderTo ) + .attr( "class", "scatterplot" ) + //.attr( "width", config.width + ( config.margin.right + config.margin.left ) ) + .attr( "width", '100%' ) + .attr( "height", config.height + ( config.margin.top + config.margin.bottom ) ); + + var content = svg.append( "g" ) + .attr( "class", "content" ) + .attr( "transform", translateStr( config.margin.left, config.margin.top ) ) + .call( zoom ); + + // a BIG gotcha - zoom (or any mouse/touch event in SVG?) requires the pointer to be over an object + // create a transparent rect to be that object here + content.append( 'rect' ) + .attr( "class", "zoom-rect" ) + .attr( "width", config.width ).attr( "height", config.height ) + .style( "fill", "transparent" ); + + //console.log( 'svg:', svg, 'content:', content ); + + // .................................................................... axes + var axis = { x : {}, y : {} }; + //console.log( 'x.ticks:', config.x.ticks ); + //console.log( 'y.ticks:', config.y.ticks ); + axis.x.fn = d3.svg.axis() + .orient( 'bottom' ) + .scale( interpolaterFns.x ) + .ticks( config.x.ticks ) + // this will convert thousands -> k, millions -> M, etc. + .tickFormat( d3.format( 's' ) ); + + axis.y.fn = d3.svg.axis() + .orient( 'left' ) + .scale( interpolaterFns.y ) + .ticks( config.y.ticks ) + .tickFormat( d3.format( 's' ) ); + + axis.x.g = content.append( 'g' ) + .attr( 'class', 'x axis' ) + .attr( 'transform', translateStr( 0, config.height ) ) + .call( axis.x.fn ); + //console.log( 'axis.x.g:', axis.x.g ); + + axis.y.g = content.append( 'g' ) + .attr( 'class', 'y axis' ) + .call( axis.y.fn ); + //console.log( 'axis.y.g:', axis.y.g ); + + // ................................ axis labels + var padding = 4; + // x-axis label + axis.x.label = svg.append( 'text' ) + .attr( 'class', 'axis-label' ) + .text( config.x.label ) + // align to the top-middle + .attr( 'text-anchor', 'middle' ) + .attr( 'dominant-baseline', 'text-after-edge' ) + .attr( 'x', ( config.width / 2 ) + config.margin.left ) + // place 4 pixels below the axis bounds + .attr( 'y', ( config.height + config.margin.bottom + config.margin.top ) - padding ); + //console.log( 'axis.x.label:', axis.x.label ); + +//TODO: anchor to left of x margin/graph + // y-axis label + // place 4 pixels left of the axis.y.g left edge + axis.y.label = svg.append( 'text' ) + .attr( 'class', 'axis-label' ) + .text( config.y.label ) + // align to bottom-middle + .attr( 'text-anchor', 'middle' ) + .attr( 'dominant-baseline', 'text-before-edge' ) + .attr( 'x', padding ) + .attr( 'y', config.height / 2 ) + // rotate around the alignment point + .attr( 'transform', rotateStr( -90, padding, config.height / 2 ) ); + //console.log( 'axis.y.label:', axis.y.label ); + + axis.redraw = function _redrawAxis(){ + svg.select( ".x.axis" ).call( axis.x.fn ); + svg.select( ".y.axis" ).call( axis.y.fn ); }; - 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 ); + // .................................................................... grid + function renderGrid(){ + var grid = { v : {}, h: {} }; + // vertical + grid.v.lines = content.selectAll( 'line.v-grid-line' ) + // data are the axis ticks; enter, update, exit + .data( interpolaterFns.x.ticks( axis.x.fn.ticks()[0] ) ); + // enter: append any extra lines needed (more ticks) + grid.v.lines.enter() + .append( 'svg:line' ) + .classed( 'grid-line v-grid-line', true ); + // update: set coords + grid.v.lines + .attr( 'x1', interpolaterFns.x ) + .attr( 'x2', interpolaterFns.x ) + .attr( 'y1', 0 ) + .attr( 'y2', config.height ); + // exit: just remove them + grid.v.lines.exit().remove(); + //console.log( 'grid.v.lines:', grid.v.lines ); + + // horizontal + grid.h.lines = content.selectAll( 'line.h-grid-line' ) + .data( interpolaterFns.y.ticks( axis.y.fn.ticks()[0] ) ); + grid.h.lines.enter() + .append( 'svg:line' ) + .classed( 'grid-line h-grid-line', true ); + grid.h.lines + .attr( 'x1', 0 ) + .attr( 'x2', config.width ) + .attr( 'y1', interpolaterFns.y ) + .attr( 'y2', interpolaterFns.y ); + grid.h.lines.exit().remove(); + //console.log( 'grid.h.lines:', grid.h.lines ); + return grid; + } + var grid = renderGrid(); + + //// .................................................................... datapoints + var datapoints = content.selectAll( '.glyph' ).data( data ) + // enter - NEW data to be added as glyphs + .enter().append( 'svg:circle' ) + .classed( "glyph", true ) + .attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); }) + // give them a 'entry' position and style + .attr( "cy", config.height ) + .attr( "r", 0 ); + + // for all EXISTING glyphs and those that need to be added: transition anim to final state + datapoints.transition().duration( config.animDuration ) + .attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); }) + .attr( "r", config.datapointSize ); + //console.log( 'datapoints:', datapoints ); + + function _redrawDatapointsClipped(){ + return datapoints + .attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); }) + .attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); }) + .style( 'display', 'block' ) + // filter out points now outside the graph content area and hide them + .filter( function( d, i ){ + var cx = d3.select( this ).attr( "cx" ), + cy = d3.select( this ).attr( "cy" ); + if( cx < 0 || cx > config.width ){ return true; } + if( cy < 0 || cy > config.height ){ return true; } + return false; + }).style( 'display', 'none' ); + } + + // .................................................................... behaviors + function zoomed( scale, translateX, translateY ){ + //console.debug( 'zoom', this, scale, translateX, translateY, arguments ); + // re-render axis, grid, and datapoints + axis.redraw(); + _redrawDatapointsClipped(); + grid = renderGrid(); + $( '.chart-info-box' ).remove(); + $( svg.node() ).trigger( 'zoom.scatterplot', [] ); + } + //TODO: programmatically set zoom/pan and save in config + //TODO: set pan/zoom limits + zoom.on( "zoom", zoomed ); + + + function infoBox( top, left, d ){ + // create an abs pos. element containing datapoint data (d) near the point (top, left) + // with added padding to clear the mouse pointer + left += 8; + return $([ + '<div class="chart-info-box" style="position: absolute">', + (( config.idColumn )?( '<div>' + d[ config.idColumn ] + '</div>' ):( '' )), + '<div>', getX( d ), '</div>', + '<div>', getY( d ), '</div>', + '</div>' + ].join( '' ) ).css({ top: top, left: left, 'z-index': 2 }); + } + + datapoints.on( 'mouseover', function( d, i ){ + var datapoint = d3.select( this ); + datapoint + .style( 'fill', 'red' ) + .style( 'fill-opacity', 1 ); + + // create horiz line to axis + 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' ) - config.datapointSize ) + .attr( 'y1', datapoint.attr( 'cy' ) ) + .attr( 'x2', 0 ) + .attr( 'y2', datapoint.attr( 'cy' ) ) + .classed( 'hoverline', true ); + + // create vertical line to axis - if not on the x axis + if( datapoint.attr( 'cy' ) < config.height ){ + content.append( 'line' ) + .attr( 'stroke', 'red' ) + .attr( 'stroke-width', 1 ) + .attr( 'x1', datapoint.attr( 'cx' ) ) + // attributes are strings so, (accrd. to js) '3' - 1 = 2 but '3' + 1 = '31': coerce + .attr( 'y1', +datapoint.attr( 'cy' ) + config.datapointSize ) + .attr( 'x2', datapoint.attr( 'cx' ) ) + .attr( 'y2', config.height ) + .classed( 'hoverline', true ); } - //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 ] ); - }; + // show the info box and trigger an event + var bbox = this.getBoundingClientRect(); + $( 'body' ).append( infoBox( bbox.top, bbox.right, d ) ); + $( svg.node() ).trigger( 'mouseover-datapoint.scatterplot', [ this, d, 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; - }; - + datapoints.on( 'mouseout', function(){ + // return the point to normal, remove hoverlines and info box + d3.select( this ) + .style( 'fill', 'black' ) + .style( 'fill-opacity', 0.2 ); + content.selectAll( '.hoverline' ).remove(); + $( '.chart-info-box' ).remove(); + }); } //============================================================================== This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org