--- a/js/flotr2/js/plugins/spreadsheet.js +++ b/js/flotr2/js/plugins/spreadsheet.js @@ -1,1 +1,297 @@ - +/** 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('
'), + graph = D.node('
'+this.options.spreadsheet.tabGraphLabel+'
'), + data = D.node('
'+this.options.spreadsheet.tabDataLabel+'
'), + 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 = [''], + buttonDownload, buttonSelect, t; + + // First row : series' labels + var html = ['']; + html.push(''); + _.each(s, function(serie,i){ + html.push(''); + colgroup.push(''); + }); + html.push(''); + // Data rows + _.each(datagrid, function(row){ + html.push(''); + _.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+''); + }, this); + html.push(''); + }, this); + colgroup.push(''); + 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( + ''); + + buttonSelect = D.node( + ''); + + this. + observe(buttonDownload, 'click', _.bind(this.spreadsheet.downloadCSV, this)). + observe(buttonSelect, 'click', _.bind(this.spreadsheet.selectAllData, this)); + + var toolbar = D.node('
'); + D.insert(toolbar, buttonDownload); + D.insert(toolbar, buttonSelect); + + var containerHeight =this.canvasHeight - D.size(this.spreadsheet.tabsContainer).height-2, + container = D.node('
'); + + 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); + } +}); +})(); +
 '+(serie.label || String.fromCharCode(65+i))+'