mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
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
This commit is contained in:
@@ -6,6 +6,7 @@ module.exports = function(grunt) {
|
||||
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 @@ module.exports = function(grunt) {
|
||||
},
|
||||
|
||||
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 @@ module.exports = function(grunt) {
|
||||
},
|
||||
|
||||
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 @@ module.exports = function(grunt) {
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="{{width}}" height="{{height}}"></svg>
|
||||
|
Before Width: | Height: | Size: 49 B |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
-46
@@ -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 }}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -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()';
|
||||
}
|
||||
});
|
||||
@@ -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-panning-in-d3-js
|
||||
//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 );
|
||||
}
|
||||
//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' )
|
||||
// .................................................................... 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 the attributes of existing and appended
|
||||
this.vGridLines
|
||||
.attr( 'x1', this.xScale )
|
||||
// update: set coords
|
||||
grid.v.lines
|
||||
.attr( 'x1', interpolaterFns.x )
|
||||
.attr( 'x2', interpolaterFns.x )
|
||||
.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 );
|
||||
.attr( 'y2', config.height );
|
||||
// exit: just remove them
|
||||
grid.v.lines.exit().remove();
|
||||
//console.log( 'grid.v.lines:', grid.v.lines );
|
||||
|
||||
// HORIZONTAL
|
||||
this.hGridLines = this.content.selectAll( 'line.h-grid-line' )
|
||||
.data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) );
|
||||
|
||||
this.hGridLines.enter().append( 'svg:line' )
|
||||
// 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 );
|
||||
|
||||
this.hGridLines
|
||||
grid.h.lines
|
||||
.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 ] );
|
||||
};
|
||||
.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();
|
||||
|
||||
//this.datapoints = this.addDatapoints( xCol, yCol, ids, ".glyph" );
|
||||
var datapoints = this.content.selectAll( '.glyph' ).data( xCol );
|
||||
//// .................................................................... 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 );
|
||||
|
||||
// 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' );
|
||||
// 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 );
|
||||
|
||||
this._addDatapointEventhandlers( datapoints, xCol, yCol, ids );
|
||||
};
|
||||
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' );
|
||||
}
|
||||
|
||||
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 );
|
||||
// .................................................................... 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 );
|
||||
|
||||
// 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 );
|
||||
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 });
|
||||
}
|
||||
|
||||
// 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();
|
||||
datapoints.on( 'mouseover', function( d, i ){
|
||||
var datapoint = d3.select( this );
|
||||
datapoint
|
||||
.style( 'fill', 'red' )
|
||||
.style( 'fill-opacity', 1 );
|
||||
|
||||
// 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 );
|
||||
// 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( '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;
|
||||
};
|
||||
// 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 ] );
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
/* =============================================================================
|
||||
todo:
|
||||
I'd like to move the svg creation out of the splot constr. to:
|
||||
allow adding splots to an existing canvas
|
||||
allow mult. splots sharing a canvas
|
||||
|
||||
|
||||
outside this:
|
||||
BUG: setting width, height in plot controls doesn't re-interpolate data locations!!
|
||||
BUG?: get metadata_column_names (from datatype if necessary)
|
||||
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
|
||||
|
||||
wire label setters, anim setter
|
||||
|
||||
TwoVarScatterplot:
|
||||
??: maybe better to do this with a canvas...
|
||||
save as visualization
|
||||
to seperate file?
|
||||
remove underscore dependencies
|
||||
add interface to change values (seperate)?
|
||||
download svg -> base64 encode
|
||||
incorporate glyphs, glyph state renderers
|
||||
|
||||
ScatterplotSettingsForm:
|
||||
some css bug that lowers the width of settings form when plot-controls tab is open
|
||||
causes chart to shift
|
||||
what can be abstracted/reused for other graphs?
|
||||
avoid direct manipulation of this.plot
|
||||
allow option to put plot into seperate tab of interface (for small multiples)
|
||||
|
||||
provide callback in view to load data incrementally - for large sets
|
||||
paginate
|
||||
handle rerender
|
||||
use endpoint (here and on the server (fileptr))
|
||||
fetch (new?) data
|
||||
handle rerender
|
||||
use d3.TSV?
|
||||
render warning on long data (> maxDataPoints)
|
||||
adjust endpoint
|
||||
|
||||
selectable list of preset column comparisons (rnaseq etc.)
|
||||
how to know what sort of Tabular the data is?
|
||||
smarter about headers
|
||||
validate columns selection (here or server)
|
||||
|
||||
set stats column names by selected columns
|
||||
move chart into tabbed area...
|
||||
|
||||
Scatterplot.mako:
|
||||
multiple plots on one page (small multiples)
|
||||
?? ensure svg styles thru d3 or css?
|
||||
d3: configable (easily)
|
||||
css: standard - better maintenance
|
||||
? override at config
|
||||
|
||||
============================================================================= */
|
||||
/**
|
||||
* Scatterplot control UI as a backbone view
|
||||
* handles:
|
||||
* getting the desired data
|
||||
* configuring the plot display
|
||||
* showing (general) statistics
|
||||
*
|
||||
* initialize attributes REQUIRES a dataset and an apiDatasetsURL
|
||||
*/
|
||||
var ScatterplotControlForm = 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
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -1,19 +1,27 @@
|
||||
<script type="text/template" class="template-visualization" id="template-visualization-scatterplotControlForm">
|
||||
{{! main layout }}
|
||||
|
||||
<h1>WHAAAAA?</h1>
|
||||
<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 }}
|
||||
<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>
|
||||
<li>
|
||||
<a title="This tab will display overall statistics for your data"
|
||||
href="#stats-display" data-toggle="tab">Statistics</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="This tab will display the chart"
|
||||
href="#chart-display" data-toggle="tab">Chart</a>
|
||||
|
||||
<div id="loading-indicator" style="display: none;">
|
||||
<img class="loading-img" src="{{loadingIndicatorImagePath}}" />
|
||||
<span class="loading-message">{{message}}</span>
|
||||
@@ -35,12 +43,19 @@
|
||||
|
||||
{{! ---------------------------- tab for data statistics }}
|
||||
<div id="stats-display" class="tab-pane">
|
||||
{{! rendered separately }}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{{! ---------------------------- tab for actual chart }}
|
||||
<div id="chart-display" class="tab-pane">
|
||||
{{! chart rendered separately }}
|
||||
<svg width="{{width}}" height="{{height}}"></svg>
|
||||
</div>
|
||||
|
||||
</div>{{! end .tab-content }}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
var numericColumnStats = function( data, keys ){
|
||||
var console = console || { debug: function(){} },
|
||||
stats = {};
|
||||
|
||||
// precondition: keys is an array of keys accessible on each data in data
|
||||
if( !keys || !keys.length ){
|
||||
throw new Error( 'keys is a required parameter and must be an array:' + keys );
|
||||
}
|
||||
if( !data || !data.length ){
|
||||
return stats;
|
||||
}
|
||||
|
||||
function parseVal( val ){
|
||||
if( val === null ){
|
||||
throw new Error( 'Null value' );
|
||||
}
|
||||
val = Number( val );
|
||||
if( isNaN( val ) || !isFinite( val ) ){
|
||||
throw new Error( 'NaN or non-finite number' );
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// does this effect the data in the other thread?
|
||||
[ 'min', 'max', 'sum', 'mean', 'median', 'count' ].forEach( function( name, i ){
|
||||
stats[ name ] = new Array( keys.length );
|
||||
});
|
||||
|
||||
var keyIndex = 0,
|
||||
separatedCols = new Array( keys.length );
|
||||
keys.forEach( function( key, keyIndex ){
|
||||
stats.min[ keyIndex ] = 0;
|
||||
stats.max[ keyIndex ] = 0;
|
||||
stats.sum[ keyIndex ] = 0;
|
||||
separatedCols[ keyIndex ] = [];
|
||||
});
|
||||
|
||||
// work backwards to prevent co-modification problems while splitting up data into columns
|
||||
for( var dataIndex=( data.length - 1 ); dataIndex>=0; dataIndex-=1 ){
|
||||
var datum = data.pop( dataIndex );
|
||||
|
||||
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
|
||||
var key = keys[ keyIndex ],
|
||||
datumColVal = datum[ key ];
|
||||
|
||||
try {
|
||||
//NOTE: parsing value as number
|
||||
datumColVal = parseVal( datumColVal );
|
||||
} catch( e ){
|
||||
continue;
|
||||
}
|
||||
|
||||
// separate the columns
|
||||
separatedCols[ keyIndex ].unshift( datumColVal );
|
||||
// get the other stats
|
||||
stats.min[ keyIndex ] = Math.min( stats.min[ keyIndex ], datumColVal );
|
||||
stats.max[ keyIndex ] = Math.max( stats.max[ keyIndex ], datumColVal );
|
||||
stats.sum[ keyIndex ] += datumColVal;
|
||||
}
|
||||
}
|
||||
|
||||
// get counts, mean, median
|
||||
function comparator( a, b ){
|
||||
if( a < b ){ return -1; }
|
||||
if( a < b ){ return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
|
||||
var count = separatedCols[ keyIndex ].length,
|
||||
sum = stats.sum[ keyIndex ];
|
||||
stats.count[ keyIndex ] = count;
|
||||
stats.mean[ keyIndex ] = ( sum / count );
|
||||
|
||||
// sort columns for median
|
||||
separatedCols[ keyIndex ].sort( comparator );
|
||||
|
||||
// odd count -> straight forward median
|
||||
var middleDataIndex = Math.floor( count / 2 );
|
||||
if( count % 2 === 1 ){
|
||||
stats.median[ keyIndex ] = separatedCols[ keyIndex ][ middleDataIndex ];
|
||||
} else {
|
||||
var middleValA = separatedCols[ keyIndex ][ middleDataIndex ],
|
||||
middleValB = separatedCols[ keyIndex ][( middleDataIndex + 1 )];
|
||||
stats.median[ keyIndex ] = ( middleValA + middleValB ) / 2;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,171 +1,199 @@
|
||||
/*TODO: use/move into base.less*/
|
||||
* { margin: 0px; padding: 0px; }
|
||||
|
||||
/* -------------------------------------------- general layout */
|
||||
div.tab-pane {
|
||||
padding: 8px;
|
||||
}
|
||||
* { margin: 0; padding: 0; }
|
||||
|
||||
/* -------------------------------------------- header */
|
||||
.header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.chart-header > * { margin: 0; padding: 0; }
|
||||
|
||||
#chart-header {
|
||||
padding : 8px;
|
||||
background-color: #ebd9b2;
|
||||
.chart-header {
|
||||
margin-bottom: 16px;
|
||||
overflow: auto;
|
||||
background-color: #ebd9b2;
|
||||
padding : 16px 12px 12px 12px;
|
||||
}
|
||||
|
||||
#chart-header .subtitle {
|
||||
margin: -4px 0px 0px 4px;
|
||||
padding : 0;
|
||||
.chart-header h2 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.chart-header p {
|
||||
color: white;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- main layout */
|
||||
#scatterplot {
|
||||
/*from width + margin of chart?*/
|
||||
.scatterplot-editor > * { margin: 0; padding: 0; }
|
||||
.scatterplot-editor .tab-pane {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.scatterplot-container .tab-pane {
|
||||
.scatterplot-editor .help-text,
|
||||
.scatterplot-editor .help-text-small {
|
||||
color: grey;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- all controls */
|
||||
.scatterplot-editor .help-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#scatterplot input[type=button],
|
||||
#scatterplot select {
|
||||
.scatterplot-editor .help-text-small {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* ============================================ config controls */
|
||||
.scatterplot-config-control {
|
||||
max-width: 768px;
|
||||
}
|
||||
|
||||
.scatterplot-config-control input[type="button"],
|
||||
.scatterplot-config-control button,
|
||||
.scatterplot-config-control select {
|
||||
width: 100%;
|
||||
max-width: 256px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#scatterplot .help-text,
|
||||
#scatterplot .help-text-small {
|
||||
color: grey;
|
||||
.scatterplot-config-control input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 512px;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
#scatterplot .help-text {
|
||||
padding-bottom: 16px;
|
||||
.scatterplot-config-control input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
#scatterplot .help-text-small {
|
||||
padding: 4px;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#scatterplot > * {
|
||||
}
|
||||
|
||||
#scatterplot input[value=Draw] {
|
||||
.scatterplot-config-control .render-button {
|
||||
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;*/
|
||||
.scatterplot-config-control .form-input {
|
||||
margin-bottom: 8px;
|
||||
/*display: table-cell;*/
|
||||
}
|
||||
|
||||
#chart-control .slider {
|
||||
/*display: table-cell;*/
|
||||
.scatterplot-config-control .slider {
|
||||
height: 8px;
|
||||
display: block;
|
||||
margin: 8px 0px 0px 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#chart-control .slider-output {
|
||||
/*display: table-cell;*/
|
||||
.scatterplot-config-control .slider-output {
|
||||
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;
|
||||
}
|
||||
|
||||
/* ============================================ plot display */
|
||||
/* -------------------------------------------- load indicators */
|
||||
#loading-indicator {
|
||||
margin: 12px 0px 0px 8px;
|
||||
.scatterplot-display .loading-indicator {
|
||||
position: absolute;
|
||||
margin: 10px 0px 0px 10px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
#scatterplot #loading-indicator .loading-message {
|
||||
.scatterplot-display .loading-indicator .loading-indicator-message {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
font-style: italic;
|
||||
font-size: smaller;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- display controls */
|
||||
.scatterplot-display .controls {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 3px;
|
||||
padding: 8px 8px 4px 8px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls button {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls .left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls .pagination {
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls .data-prev-next {
|
||||
display: inline-block;
|
||||
margin: 0px 4px 0px 0px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls .pagination-container {
|
||||
display: inline-block;
|
||||
overflow: auto;
|
||||
max-width: 256px;
|
||||
/*very tweaky*/
|
||||
margin: 0px 8px 0px 0px;
|
||||
height: 27px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.scatterplot-display .controls .pagination-container a {
|
||||
display: inline;
|
||||
/* WTH, bootstrap? */
|
||||
position: static;
|
||||
float: none;
|
||||
/* up from 4px -> 5px */
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls .right {
|
||||
float: right;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.scatterplot-display .controls p {
|
||||
display: inline-block;
|
||||
margin: 0px 4px 0px 4px;
|
||||
/*to better match the buttons*/
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.scatterplot-display button.rerender-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.scatterplot-display button.stats-toggle-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- chart area */
|
||||
#chart-holder {
|
||||
overflow: auto;
|
||||
margin-left: 8px;
|
||||
svg.scatterplot {
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
svg .grid-line {
|
||||
svg.scatterplot .grid-line {
|
||||
fill: none;
|
||||
stroke: lightgrey;
|
||||
stroke-opacity: 0.5;
|
||||
stroke-opacity: 0.2;
|
||||
shape-rendering: crispEdges;
|
||||
stroke-dasharray: 3, 3;
|
||||
}
|
||||
|
||||
svg .axis path, svg .axis line {
|
||||
svg.scatterplot .axis path,
|
||||
svg.scatterplot .axis line {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
svg .axis text {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
svg.scatterplot .axis text {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
svg #x-axis-label, svg #y-axis-label {
|
||||
svg.scatterplot .axis-label {
|
||||
font-family: sans-serif;
|
||||
font-size: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
svg .glyph {
|
||||
svg.scatterplot .glyph {
|
||||
stroke: none;
|
||||
fill: black;
|
||||
fill-opacity: 0.2;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
onmessage = function( event ){
|
||||
importScripts( 'numeric-column-stats.js' );
|
||||
postMessage( numericColumnStats( event.data.data, event.data.keys ) );
|
||||
self.close();
|
||||
};
|
||||
@@ -23,45 +23,43 @@
|
||||
<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>
|
||||
|
||||
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot-edit.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 class="chart-header">
|
||||
<h2>Scatterplot of '${hda.name}'</h2>
|
||||
<p>${hda.info}</p>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<div id="scatterplot" class="scatterplot-control-form"></div>
|
||||
|
||||
<div class="scatterplot-editor"></div>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
<%
|
||||
# optionally, bootstrap data from dprov
|
||||
data = None
|
||||
##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) )
|
||||
%>
|
||||
var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )},
|
||||
data = ${h.to_json_string( data )},
|
||||
querySettings = ${h.to_json_string( query_args )},
|
||||
chartConfig = _.extend( querySettings, {
|
||||
config = _.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
|
||||
dataset : hda,
|
||||
});
|
||||
//console.debug( querySettings );
|
||||
|
||||
var settingsForm = new ScatterplotControlForm({
|
||||
dataset : hda,
|
||||
apiDatasetsURL : "${h.url_for( controller='/api/datasets', action='index' )}",
|
||||
el : $( '#scatterplot' ),
|
||||
chartConfig : chartConfig
|
||||
var editor = new ScatterplotConfigEditor({
|
||||
el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hda.id ),
|
||||
config : config
|
||||
}).render();
|
||||
|
||||
// uncomment to auto render for development
|
||||
//$( '.render-button:visible' ).click();
|
||||
});
|
||||
</script>
|
||||
%endif
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user