PageSourceSearch

https://infovis.co/js/tabletop.js

js infovis.co collected 2026-09-24 10:41:26 UTC 18,247 bytes, 567 lines download raw bytes

1(function(global) {
2  "use strict";
3
4  var inNodeJS = false;
5  if (typeof module !== 'undefined' && module.exports) {
6    inNodeJS = true;
7    var request = require('request');
8  }
9
10  var supportsCORS = false;
11  var inLegacyIE = false;
12  try {
13    var testXHR = new XMLHttpRequest();
14    if (typeof testXHR.withCredentials !== 'undefined') {
15      supportsCORS = true;
16    } else {
17      if ("XDomainRequest" in window) {
18        supportsCORS = true;
19        inLegacyIE = true;
20      }
21    }
22  } catch (e) { }
23
24  // Create a simple indexOf function for support
25  // of older browsers.  Uses native indexOf if 
26  // available.  Code similar to underscores.
27  // By making a separate function, instead of adding
28  // to the prototype, we will not break bad for loops
29  // in older browsers
30  var indexOfProto = Array.prototype.indexOf;
31  var ttIndexOf = function(array, item) {
32    var i = 0, l = array.length;
33    
34    if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item);
35    for (; i < l; i++) if (array[i] === item) return i;
36    return -1;
37  };
38  
39  /*
40    Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } )
41      OR!
42    Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } )
43      OR!
44    Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc')
45  */
46
47  var Tabletop = function(options) {
48    // Make sure Tabletop is being used as a constructor no matter what.
49    if(!this || !(this instanceof Tabletop)) {
50      return new Tabletop(options);
51    }
52    
53    if(typeof(options) === 'string') {
54      options = { key : options };
55    }
56
57    this.callback = options.callback;
58    this.wanted = options.wanted || [];
59    this.key = options.key;
60    this.simpleSheet = !!options.simpleSheet;
61    this.parseNumbers = !!options.parseNumbers;
62    this.wait = !!options.wait;
63    this.reverse = !!options.reverse;
64    this.postProcess = options.postProcess;
65    this.debug = !!options.debug;
66    this.query = options.query || '';
67    this.orderby = options.orderby;
68    this.endpoint = options.endpoint || "https://spreadsheets.google.com";
69    this.singleton = !!options.singleton;
70    this.simple_url = !!options.simple_url;
71    this.callbackContext = options.callbackContext;
72    // Default to on, unless there's a proxy, in which case it's default off
73    this.prettyColumnNames = typeof(options.prettyColumnNames) == 'undefined' ? !options.proxy : options.prettyColumnNames
74    
75    if(typeof(options.proxy) !== 'undefined') {
76      // Remove trailing slash, it will break the app
77      this.endpoint = options.proxy.replace(/\/$/,'');
78      this.simple_url = true;
79      this.singleton = true;
80      // Let's only use CORS (straight JSON request) when
81      // fetching straight from Google
82      supportsCORS = false;
83    }
84    
85    this.parameterize = options.parameterize || false;
86    
87    if(this.singleton) {
88      if(typeof(Tabletop.singleton) !== 'undefined') {
89        this.log("WARNING! Tabletop singleton already defined");
90      }
91      Tabletop.singleton = this;
92    }
93    
94    /* Be friendly about what you accept */
95    if(/key=/.test(this.key)) {
96      this.log("You passed an old Google Docs url as the key! Attempting to parse.");
97      this.key = this.key.match("key=(.*?)(&|#|$)")[1];
98    }
99
100    if(/pubhtml/.test(this.key)) {
101      this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse.");
102      this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1];
103    }
104
105    if(!this.key) {
106      this.log("You need to pass Tabletop a key!");
107      return;
108    }
109
110    this.log("Initializing with key " + this.key);
111
112    this.models = {};
113    this.model_names = [];
114
115    this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt=";
116
117    if (inNodeJS || supportsCORS) {
118      this.base_json_path += 'json';
119    } else {
120      this.base_json_path += 'json-in-script';
121    }
122    
123    if(!this.wait) {
124      this.fetch();
125    }
126  };
127
128  // A global storage for callbacks.
129  Tabletop.callbacks = {};
130
131  // Backwards compatibility.
132  Tabletop.init = function(options) {
133    return new Tabletop(options);
134  };
135
136  Tabletop.sheets = function() {
137    this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)");
138  };
139
140  Tabletop.prototype = {
141
142    fetch: function(callback) {
143      if(typeof(callback) !== "undefined") {
144        this.callback = callback;
145      }
146      this.requestData(this.base_json_path, this.loadSheets);
147    },
148    
149    /*
150      This will call the environment appropriate request method.
151      
152      In browser it will use JSON-P, in node it will use request()
153    */
154    requestData: function(path, callback) {
155      if (inNodeJS) {
156        this.serverSideFetch(path, callback);
157      } else {
158        //CORS only works in IE8/9 across the same protocol
159        //You must have your server on HTTPS to talk to Google, or it'll fall back on injection
160        var protocol = this.endpoint.split("//").shift() || "http";
161        if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) {
162          this.xhrFetch(path, callback);
163        } else {
164          this.injectScript(path, callback);
165        }
166      }
167    },
168
169    /*
170      Use Cross-Origin XMLHttpRequest to get the data in browsers that support it.
171    */
172    xhrFetch: function(path, callback) {
173      //support IE8's separate cross-domain object
174      var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest();
175      xhr.open("GET", this.endpoint + path);
176      var self = this;
177      xhr.onload = function() {
178        try {
179          var json = JSON.parse(xhr.responseText);
180        } catch (e) {
181          console.error(e);
182        }
183        callback.call(self, json);
184      };
185      xhr.send();
186    },
187    
188    /*
189      Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data
190      it triggers the callback. This helps you avoid cross-domain errors
191      http://code.google.com/apis/gdata/samples/spreadsheet_sample.html
192
193      Let's be plain-Jane and not use jQuery or anything.
194    */
195    injectScript: function(path, callback) {
196      var script = document.createElement('script');
197      var callbackName;
198      
199      if(this.singleton) {
200        if(callback === this.loadSheets) {
201          callbackName = 'Tabletop.singleton.loadSheets';
202        } else if (callback === this.loadSheet) {
203          callbackName = 'Tabletop.singleton.loadSheet';
204        }
205      } else {
206        var self = this;
207        callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000));
208        // Create a temp callback which will get removed once it has executed,
209        // this allows multiple instances of Tabletop to coexist.
210        Tabletop.callbacks[ callbackName ] = function () {
211          var args = Array.prototype.slice.call( arguments, 0 );
212          callback.apply(self, args);
213          script.parentNode.removeChild(script);
214          delete Tabletop.callbacks[callbackName];
215        };
216        callbackName = 'Tabletop.callbacks.' + callbackName;
217      }
218      
219      var url = path + "&callback=" + callbackName;
220      
221      if(this.simple_url) {
222        // We've gone down a rabbit hole of passing injectScript the path, so let's
223        // just pull the sheet_id out of the path like the least efficient worker bees
224        if(path.indexOf("/list/") !== -1) {
225          script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4];
226        } else {
227          script.src = this.endpoint + "/" + this.key;
228        }
229      } else {
230        script.src = this.endpoint + url;
231      }
232      
233      if (this.parameterize) {
234        script.src = this.parameterize + encodeURIComponent(script.src);
235      }
236      
237      document.getElementsByTagName('script')[0].parentNode.appendChild(script);
238    },
239    
240    /* 
241      This will only run if tabletop is being run in node.js
242    */
243    serverSideFetch: function(path, callback) {
244      var self = this
245      request({url: this.endpoint + path, json: true}, function(err, resp, body) {
246        if (err) {
247          return console.error(err);
248        }
249        callback.call(self, body);
250      });
251    },
252
253    /* 
254      Is this a sheet you want to pull?
255      If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported
256      Pulls all sheets if none are specified
257    */
258    isWanted: function(sheetName) {
259      if(this.wanted.length === 0) {
260        return true;
261      } else {
262        return (ttIndexOf(this.wanted, sheetName) !== -1);
263      }
264    },
265    
266    /*
267      What gets send to the callback
268      if simpleSheet === true, then don't return an array of Tabletop.this.models,
269      only return the first one's elements
270    */
271    data: function() {
272      // If the instance is being queried before the data's been fetched
273      // then return undefined.
274      if(this.model_names.length === 0) {
275        return undefined;
276      }
277      if(this.simpleSheet) {
278        if(this.model_names.length > 1 && this.debug) {
279          this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong.");
280        }
281        return this.models[ this.model_names[0] ].all();
282      } else {
283        return this.models;
284      }
285    },
286
287    /*
288      Add another sheet to the wanted list
289    */
290    addWanted: function(sheet) {
291      if(ttIndexOf(this.wanted, sheet) === -1) {
292        this.wanted.push(sheet);
293      }
294    },
295    
296    /*
297      Load all worksheets of the spreadsheet, turning each into a Tabletop Model.
298      Need to use injectScript because the worksheet view that you're working from
299      doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though.
300      Calls back to loadSheet in order to get the real work done.
301
302      Used as a callback for the worksheet-based JSON
303    */
304    loadSheets: function(data) {
305      var i, ilen;
306      var toLoad = [];
307      this.foundSheetNames = [];
308
309      for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) {
310        this.foundSheetNames.push(data.feed.entry[i].title.$t);
311        // Only pull in desired sheets to reduce loading
312        if( this.isWanted(data.feed.entry[i].content.$t) ) {
313          var linkIdx = data.feed.entry[i].link.length-1;
314          var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop();
315          var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt="
316          if (inNodeJS || supportsCORS) {
317            json_path += 'json';
318          } else {
319            json_path += 'json-in-script';
320          }
321          if(this.query) {
322            json_path += "&sq=" + this.query;
323          }
324          if(this.orderby) {
325            json_path += "&orderby=column:" + this.orderby.toLowerCase();
326          }
327          if(this.reverse) {
328            json_path += "&reverse=true";
329          }
330          toLoad.push(json_path);
331        }
332      }
333
334      this.sheetsToLoad = toLoad.length;
335      for(i = 0, ilen = toLoad.length; i < ilen; i++) {
336        this.requestData(toLoad[i], this.loadSheet);
337      }
338    },
339
340    /*
341      Access layer for the this.models
342      .sheets() gets you all of the sheets
343      .sheets('Sheet1') gets you the sheet named Sheet1
344    */
345    sheets: function(sheetName) {
346      if(typeof sheetName === "undefined") {
347        return this.models;
348      } else {
349        if(typeof(this.models[ sheetName ]) === "undefined") {
350          // alert( "Can't find " + sheetName );
351          return;
352        } else {
353          return this.models[ sheetName ];
354        }
355      }
356    },
357
358    sheetReady: function(model) {
359      this.models[ model.name ] = model;
360      if(ttIndexOf(this.model_names, model.name) === -1) {
361        this.model_names.push(model.name);
362      }
363
364      this.sheetsToLoad--;
365      if(this.sheetsToLoad === 0)
366        this.doCallback();
367    },
368    
369    /*
370      Parse a single list-based worksheet, turning it into a Tabletop Model
371
372      Used as a callback for the list-based JSON
373    */
374    loadSheet: function(data) {
375      var that = this;
376      var model = new Tabletop.Model( { data: data, 
377                                        parseNumbers: this.parseNumbers,
378                                        postProcess: this.postProcess,
379                                        tabletop: this,
380                                        prettyColumnNames: this.prettyColumnNames,
381                                        onReady: function() {
382                                          that.sheetReady(this);
383                                        } } );
384    },
385
386    /*
387      Execute the callback upon loading! Rely on this.data() because you might
388        only request certain pieces of data (i.e. simpleSheet mode)
389      Tests this.sheetsToLoad just in case a race condition happens to show up
390    */
391    doCallback: function() {
392      if(this.sheetsToLoad === 0) {
393        this.callback.apply(this.callbackContext || this, [this.data(), this]);
394      }
395    },
396
397    log: function(msg) {
398      if(this.debug) {
399        if(typeof console !== "undefined" && typeof console.log !== "undefined") {
400          Function.prototype.apply.apply(console.log, [console, arguments]);
401        }
402      }
403    }
404
405  };
406
407  /*
408    Tabletop.Model stores the attribute names and parses the worksheet data
409      to turn it into something worthwhile
410
411    Options should be in the format { data: XXX }, with XXX being the list-based worksheet
412  */
413  Tabletop.Model = function(options) {
414    var i, j, ilen, jlen;
415    this.column_names = [];
416    this.name = options.data.feed.title.$t;
417    this.tabletop = options.tabletop;
418    this.elements = [];
419    this.onReady = options.onReady;
420    this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae
421
422    if(typeof(options.data.feed.entry) === 'undefined') {
423      options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers");
424      this.original_columns = [];
425      this.elements = [];
426      this.onReady.call(this);
427      return;
428    }
429    
430    for(var key in options.data.feed.entry[0]){
431      if(/^gsx/.test(key))
432        this.column_names.push( key.replace("gsx$","") );
433    }
434
435    this.original_columns = this.column_names;
436    
437    for(i = 0, ilen =  options.data.feed.entry.length ; i < ilen; i++) {
438      var source = options.data.feed.entry[i];
439      var element = {};
440      for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) {
441        var cell = source[ "gsx$" + this.column_names[j] ];
442        if (typeof(cell) !== 'undefined') {
443          if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t))
444            element[ this.column_names[j] ] = +cell.$t;
445          else
446            element[ this.column_names[j] ] = cell.$t;
447        } else {
448            element[ this.column_names[j] ] = '';
449        }
450      }
451      if(element.rowNumber === undefined)
452        element.rowNumber = i + 1;
453      if( options.postProcess )
454        options.postProcess(element);
455      this.elements.push(element);
456    }
457    
458    if(options.prettyColumnNames)
459      this.fetchPrettyColumns();
460    else
461      this.onReady.call(this);
462  };
463
464  Tabletop.Model.prototype = {
465    /*
466      Returns all of the elements (rows) of the worksheet as objects
467    */
468    all: function() {
469      return this.elements;
470    },
471    
472    fetchPrettyColumns: function() {
473      if(!this.raw.feed.link[3])
474        return this.ready();
475      var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', '');
476      var that = this;
477      this.tabletop.requestData(cellurl, function(data) {
478        that.loadPrettyColumns(data)
479      });
480    },
481    
482    ready: function() {
483      this.onReady.call(this);
484    },
485    
486    /*
487     * Store column names as an object
488     * with keys of Google-formatted "columnName"
489     * and values of human-readable "Column name"
490     */
491    loadPrettyColumns: function(data) {
492      var pretty_columns = {};
493
494      var column_names = this.column_names;
495
496      var i = 0;
497      var l = column_names.length;
498
499      for (; i < l; i++) {
500        if (typeof data.feed.entry[i].content.$t !== 'undefined') {
501          pretty_columns[column_names[i]] = data.feed.entry[i].content.$t;
502        } else {
503          pretty_columns[column_names[i]] = column_names[i];
504        }
505      }
506
507      this.pretty_columns = pretty_columns;
508
509      this.prettifyElements();
510      this.ready();
511    },
512    
513    /*
514     * Go through each row, substitutiting
515     * Google-formatted "columnName"
516     * with human-readable "Column name"
517     */
518    prettifyElements: function() {
519      var pretty_elements = [],
520          ordered_pretty_names = [],
521          i, j, ilen, jlen;
522
523      var ordered_pretty_names;
524      for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
525        ordered_pretty_names.push(this.pretty_columns[this.column_names[j]]);
526      }
527
528      for(i = 0, ilen = this.elements.length; i < ilen; i++) {
529        var new_element = {};
530        for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
531          var new_column_name = this.pretty_columns[this.column_names[j]];
532          new_element[new_column_name] = this.elements[i][this.column_names[j]];
533        }
534        pretty_elements.push(new_element);
535      }
536      this.elements = pretty_elements;
537      this.column_names = ordered_pretty_names;
538    },
539
540    /*
541      Return the elements as an array of arrays, instead of an array of objects
542    */
543    toArray: function() {
544      var array = [],
545          i, j, ilen, jlen;
546      for(i = 0, ilen = this.elements.length; i < ilen; i++) {
547        var row = [];
548        for(j = 0, jlen = this.column_names.length; j < jlen ; j++) {
549          row.push( this.elements[i][ this.column_names[j] ] );
550        }
551        array.push(row);
552      }
553      return array;
554    }
555  };
556
557  if(inNodeJS) {
558    module.exports = Tabletop;
559  } else if (typeof define === 'function' && define.amd) {
560    define(function () {
561        return Tabletop;
562    });
563  } else {
564    global.Tabletop = Tabletop;
565  }
566
567})(this);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.