--- 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('
'+(serie.label || String.fromCharCode(65+i))+'
');
+ 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+''+tag+'>');
+ }, 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);
+ }
+});
+})();
+