297 lines
9.7 KiB
JavaScript
297 lines
9.7 KiB
JavaScript
/** Spreadsheet **/
|
|
(function() {
|
|
|
|
function getRowLabel(value){
|
|
if (this.options.spreadsheet.tickFormatter){
|
|
//TODO maybe pass the xaxis formatter to the custom tick formatter as an opt-out?
|
|
return this.options.spreadsheet.tickFormatter(value);
|
|
}
|
|
else {
|
|
var t = _.find(this.axes.x.ticks, function(t){return t.v == value;});
|
|
if (t) {
|
|
return t.label;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
var
|
|
D = Flotr.DOM,
|
|
_ = Flotr._;
|
|
|
|
Flotr.addPlugin('spreadsheet', {
|
|
options: {
|
|
show: false, // => show the data grid using two tabs
|
|
tabGraphLabel: 'Graph',
|
|
tabDataLabel: 'Data',
|
|
toolbarDownload: 'Download CSV', // @todo: add better language support
|
|
toolbarSelectAll: 'Select all',
|
|
csvFileSeparator: ',',
|
|
decimalSeparator: '.',
|
|
tickFormatter: null,
|
|
initialTab: 'graph'
|
|
},
|
|
/**
|
|
* Builds the tabs in the DOM
|
|
*/
|
|
callbacks: {
|
|
'flotr:afterconstruct': function(){
|
|
// @TODO necessary?
|
|
//this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove');
|
|
|
|
if (!this.options.spreadsheet.show) return;
|
|
|
|
var ss = this.spreadsheet,
|
|
container = D.node('<div class="flotr-tabs-group" style="position:absolute;left:0px;width:'+this.canvasWidth+'px"></div>'),
|
|
graph = D.node('<div style="float:left" class="flotr-tab selected">'+this.options.spreadsheet.tabGraphLabel+'</div>'),
|
|
data = D.node('<div style="float:left" class="flotr-tab">'+this.options.spreadsheet.tabDataLabel+'</div>'),
|
|
offset;
|
|
|
|
ss.tabsContainer = container;
|
|
ss.tabs = { graph : graph, data : data };
|
|
|
|
D.insert(container, graph);
|
|
D.insert(container, data);
|
|
D.insert(this.el, container);
|
|
|
|
offset = D.size(data).height + 2;
|
|
this.plotOffset.bottom += offset;
|
|
|
|
D.setStyles(container, {top: this.canvasHeight-offset+'px'});
|
|
|
|
this.
|
|
observe(graph, 'click', function(){ss.showTab('graph');}).
|
|
observe(data, 'click', function(){ss.showTab('data');});
|
|
if (this.options.spreadsheet.initialTab !== 'graph'){
|
|
ss.showTab(this.options.spreadsheet.initialTab);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Builds a matrix of the data to make the correspondance between the x values and the y values :
|
|
* X value => Y values from the axes
|
|
* @return {Array} The data grid
|
|
*/
|
|
loadDataGrid: function(){
|
|
if (this.seriesData) return this.seriesData;
|
|
|
|
var s = this.series,
|
|
rows = {};
|
|
|
|
/* The data grid is a 2 dimensions array. There is a row for each X value.
|
|
* Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)
|
|
**/
|
|
_.each(s, function(serie, i){
|
|
_.each(serie.data, function (v) {
|
|
var x = v[0],
|
|
y = v[1],
|
|
r = rows[x];
|
|
if (r) {
|
|
r[i+1] = y;
|
|
} else {
|
|
var newRow = [];
|
|
newRow[0] = x;
|
|
newRow[i+1] = y;
|
|
rows[x] = newRow;
|
|
}
|
|
});
|
|
});
|
|
|
|
// The data grid is sorted by x value
|
|
this.seriesData = _.sortBy(rows, function(row, x){
|
|
return parseInt(x, 10);
|
|
});
|
|
return this.seriesData;
|
|
},
|
|
/**
|
|
* Constructs the data table for the spreadsheet
|
|
* @todo make a spreadsheet manager (Flotr.Spreadsheet)
|
|
* @return {Element} The resulting table element
|
|
*/
|
|
constructDataGrid: function(){
|
|
// If the data grid has already been built, nothing to do here
|
|
if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
|
|
|
|
var s = this.series,
|
|
datagrid = this.spreadsheet.loadDataGrid(),
|
|
colgroup = ['<colgroup><col />'],
|
|
buttonDownload, buttonSelect, t;
|
|
|
|
// First row : series' labels
|
|
var html = ['<table class="flotr-datagrid"><tr class="first-row">'];
|
|
html.push('<th> </th>');
|
|
_.each(s, function(serie,i){
|
|
html.push('<th scope="col">'+(serie.label || String.fromCharCode(65+i))+'</th>');
|
|
colgroup.push('<col />');
|
|
});
|
|
html.push('</tr>');
|
|
// Data rows
|
|
_.each(datagrid, function(row){
|
|
html.push('<tr>');
|
|
_.times(s.length+1, function(i){
|
|
var tag = 'td',
|
|
value = row[i],
|
|
// TODO: do we really want to handle problems with floating point
|
|
// precision here?
|
|
content = (!_.isUndefined(value) ? Math.round(value*100000)/100000 : '');
|
|
if (i === 0) {
|
|
tag = 'th';
|
|
var label = getRowLabel.call(this, content);
|
|
if (label) content = label;
|
|
}
|
|
|
|
html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');
|
|
}, this);
|
|
html.push('</tr>');
|
|
}, this);
|
|
colgroup.push('</colgroup>');
|
|
t = D.node(html.join(''));
|
|
|
|
/**
|
|
* @TODO disabled this
|
|
if (!Flotr.isIE || Flotr.isIE == 9) {
|
|
function handleMouseout(){
|
|
t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
|
|
}
|
|
function handleMouseover(e){
|
|
var td = e.element(),
|
|
siblings = td.previousSiblings();
|
|
t.select('th[scope=col]')[siblings.length-1].addClassName('hover');
|
|
t.select('colgroup col')[siblings.length].addClassName('hover');
|
|
}
|
|
_.each(t.select('td'), function(td) {
|
|
Flotr.EventAdapter.
|
|
observe(td, 'mouseover', handleMouseover).
|
|
observe(td, 'mouseout', handleMouseout);
|
|
});
|
|
}
|
|
*/
|
|
|
|
buttonDownload = D.node(
|
|
'<button type="button" class="flotr-datagrid-toolbar-button">' +
|
|
this.options.spreadsheet.toolbarDownload +
|
|
'</button>');
|
|
|
|
buttonSelect = D.node(
|
|
'<button type="button" class="flotr-datagrid-toolbar-button">' +
|
|
this.options.spreadsheet.toolbarSelectAll+
|
|
'</button>');
|
|
|
|
this.
|
|
observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)).
|
|
observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this));
|
|
|
|
var toolbar = D.node('<div class="flotr-datagrid-toolbar"></div>');
|
|
D.insert(toolbar, buttonDownload);
|
|
D.insert(toolbar, buttonSelect);
|
|
|
|
var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2,
|
|
container = D.node('<div class="flotr-datagrid-container" style="position:absolute;left:0px;top:0px;width:'+
|
|
this.canvasWidth+'px;height:'+containerHeight+'px;overflow:auto;z-index:10"></div>');
|
|
|
|
D.insert(container, toolbar);
|
|
D.insert(container, t);
|
|
D.insert(this.el, container);
|
|
this.spreadsheet.datagrid = t;
|
|
this.spreadsheet.container = container;
|
|
|
|
return t;
|
|
},
|
|
/**
|
|
* Shows the specified tab, by its name
|
|
* @todo make a tab manager (Flotr.Tabs)
|
|
* @param {String} tabName - The tab name
|
|
*/
|
|
showTab: function(tabName){
|
|
if (this.spreadsheet.activeTab === tabName){
|
|
return;
|
|
}
|
|
switch(tabName) {
|
|
case 'graph':
|
|
D.hide(this.spreadsheet.container);
|
|
D.removeClass(this.spreadsheet.tabs.data, 'selected');
|
|
D.addClass(this.spreadsheet.tabs.graph, 'selected');
|
|
break;
|
|
case 'data':
|
|
if (!this.spreadsheet.datagrid)
|
|
this.spreadsheet.constructDataGrid();
|
|
D.show(this.spreadsheet.container);
|
|
D.addClass(this.spreadsheet.tabs.data, 'selected');
|
|
D.removeClass(this.spreadsheet.tabs.graph, 'selected');
|
|
break;
|
|
default:
|
|
throw 'Illegal tab name: ' + tabName;
|
|
}
|
|
this.spreadsheet.activeTab = tabName;
|
|
},
|
|
/**
|
|
* Selects the data table in the DOM for copy/paste
|
|
*/
|
|
selectAllData: function(){
|
|
if (this.spreadsheet.tabs) {
|
|
var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();
|
|
|
|
this.spreadsheet.showTab('data');
|
|
|
|
// deferred to be able to select the table
|
|
setTimeout(function () {
|
|
if ((doc = node.ownerDocument) && (win = doc.defaultView) &&
|
|
win.getSelection && doc.createRange &&
|
|
(selection = window.getSelection()) &&
|
|
selection.removeAllRanges) {
|
|
range = doc.createRange();
|
|
range.selectNode(node);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
else if (document.body && document.body.createTextRange &&
|
|
(range = document.body.createTextRange())) {
|
|
range.moveToElementText(node);
|
|
range.select();
|
|
}
|
|
}, 0);
|
|
return true;
|
|
}
|
|
else return false;
|
|
},
|
|
/**
|
|
* Converts the data into CSV in order to download a file
|
|
*/
|
|
downloadCSV: function(){
|
|
var csv = '',
|
|
series = this.series,
|
|
options = this.options,
|
|
dg = this.spreadsheet.loadDataGrid(),
|
|
separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
|
|
|
|
if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
|
|
throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
|
|
}
|
|
|
|
// The first row
|
|
_.each(series, function(serie, i){
|
|
csv += separator+'"'+(serie.label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
|
|
});
|
|
|
|
csv += "%0D%0A"; // \r\n
|
|
|
|
// For each row
|
|
csv += _.reduce(dg, function(memo, row){
|
|
var rowLabel = getRowLabel.call(this, row[0]) || '';
|
|
rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
|
|
var numbers = row.slice(1).join(separator);
|
|
if (options.spreadsheet.decimalSeparator !== '.') {
|
|
numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
|
|
}
|
|
return memo + rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
|
|
}, '', this);
|
|
|
|
if (Flotr.isIE && Flotr.isIE < 9) {
|
|
csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
|
|
window.open().document.write(csv);
|
|
}
|
|
else window.open('data:text/csv,'+csv);
|
|
}
|
|
});
|
|
})();
|