1 
  2 /**
  3  * @fileoverview Simple DataTable implementation.
  4  * @version 1.0.1
  5  * @see https://google.github.io/styleguide/jsguide.html
  6  * @see https://github.com/google/closure-compiler/wiki
  7  */
  8 
  9 
 10 
 11 /**
 12  * DataTable constructor.
 13  * @param {string|Element} container The HTML container.
 14  * @constructor
 15  * @extends {dom.EventDispatcher} dom.EventDispatcher
 16  * @requires dom.css
 17  * @requires dom.events
 18  * @requires dom.Template
 19  * @requires formatters.NumberFormatter
 20  * @requires formatters.DateFormatter
 21  * @requires util.Object
 22  * @example
 23  * <b>Simple:</b>
 24  * <b>var</b> table = <b>new</b> charts.DataTable('container_id');
 25  * table.draw([
 26  *   ['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep'], // Columns
 27  *   [100,      50,         30,         10,      40], // First row
 28  *   [140,       2,        110,        150,    1300]  // Second row
 29  * ]);
 30  *
 31  * <b>Complex:</b>
 32  * <b>var</b> options = {
 33  *   'bool-format': ['<input type="checkbox" checked>',
 34  *                   '<input type="checkbox">'],
 35  *   'date-format': 'YYYY-MM-dd'
 36  * };
 37  *
 38  * <b>var</b> table = <b>new</b> charts.DataTable('container_id');
 39  *
 40  * table.addEventListener('sort', <b>function</b>() {
 41  *   window.console <b>&&</b> console.log(table.getColumn());
 42  * });
 43  *
 44  * table.draw([
 45  *   // Columns:
 46  *   [
 47  *     {label: '', type: 'bool', name: 'checkbox'}
 48  *     {label: 'Country', title: 'Title text', name: 'country', width: '40%'},
 49  *     {label: 'Population', type: 'number', name: 'population'},
 50  *     {label: 'Date', type: 'date', name: 'date', format: 'YYYY/MM/dd'}
 51  *   ],
 52  *   // Simple row:
 53  *   [true, 'Germany', 80619000, new Date(2013, 6, 31)],
 54  *   // Complex rows:
 55  *   [0, 'USA', 317638000, {value: new Date(2014, 2, 5), format: 'YY/MM/dd'}],
 56  *   [1, {label: 'Vatican', format: '<b>{{ value }}</b>'}, 839, new Date],
 57  * ], options);
 58  *
 59  * <b>Styles: (default prefix: "data-")</b>
 60  * table.data-table {}
 61  * table.data-table caption {}
 62  * table.data-table thead tr th {}
 63  * table.data-table thead tr th span {}
 64  * table.data-table tbody tr td {}
 65  * table.data-table tfoot tr td {}
 66  * table.data-table tr.data-row-even {}
 67  * table.data-table tr.data-row-odd {}
 68  * table.data-table thead tr th.data-cell-text {}
 69  * table.data-table thead tr th.data-cell-date {}
 70  * table.data-table thead tr th.data-cell-bool {}
 71  * table.data-table thead tr th.data-cell-number {}
 72  * table.data-table tbody tr td.data-cell-text {}
 73  * table.data-table tbody tr td.data-cell-date {}
 74  * table.data-table tbody tr td.data-cell-bool {}
 75  * table.data-table tbody tr td.data-cell-number {}
 76  * table.data-table tfoot tr td.data-cell-text {}
 77  * table.data-table tfoot tr td.data-cell-date {}
 78  * table.data-table tfoot tr td.data-cell-bool {}
 79  * table.data-table tfoot tr td.data-cell-number {}
 80  * table.data-table thead th.data-sort-asc {}
 81  * table.data-table thead th.data-sort-desc {}
 82  */
 83 charts.DataTable = function(container) {
 84   dom.EventDispatcher.apply(this, arguments);
 85 
 86   /**
 87    * Default data table options.
 88    * @dict
 89    * @example <code>{
 90    *   'date-format': 'YYYY-MM-dd',      // Default date format.
 91    *   'bool-format': ['True', 'False'], // Default boolean format.
 92    *   'text-format': '{{ value }}',     // Default text template format.
 93    *   'css-prefix': 'data-',            // Default css prefix.
 94    *   'header': true,                   // Shows first row as header (THEAD).
 95    *   'footer': true,                   // Shows last row as footer (TFOOT).
 96    *   'caption': '',                    // Default table caption text.
 97    *   'sort': {
 98    *     'column': 0,                    // Default column index to sort by.
 99    *     'dir': 'asc'                    // Default sort direction.
100    *   }
101    * }</code>
102    */
103   var DEFAULT_OPTIONS = {
104     'date-format': 'YYYY-MM-dd',
105     'bool-format': ['True', 'False'],
106     'text-format': '{{ value }}',
107     'css-prefix': 'data-',
108     'header': true,
109     'footer': true,
110     'caption': '',
111     'sort': {'column': 0, 'dir': 'asc'},
112     'rows': {'offset': 0, 'limit': 0}
113   };
114 
115   /**
116    * Draws the table based on <code>data</code> and <code>opt_options</code>.
117    * @param {!Array.<Array>} data A table data.
118    * @param {Object=} opt_options A configuration options.
119    * @see <a href="#-DEFAULT_OPTIONS">DEFAULT_OPTIONS</a>
120    */
121   this.draw = function(data, opt_options) {
122     if (data instanceof Array) {
123       options_ = util.Object.extend(DEFAULT_OPTIONS, opt_options || {});
124       /** @type {DocumentFragment} */
125       var fragment = dom.document.createDocumentFragment();
126       /** @type {Node} */
127       var table = fragment.appendChild(dom.createElement('TABLE'));
128 
129       if (options_['caption']) {
130         /** @type {Node} */
131         var caption = table.appendChild(dom.createElement('CAPTION'));
132         caption.innerHTML = options_['caption'];
133       }
134 
135       /** @type {Node} */
136       var thead = options_['header'] ?
137           table.appendChild(dom.createElement('THEAD')) : null;
138       /** @type {Node} */
139       var tfoot = options_['footer'] ?
140           table.appendChild(dom.createElement('TFOOT')) : null;
141       /** @type {Node} */
142       var tbody = table.appendChild(dom.createElement('TBODY'));
143 
144       draw_(data, thead, tbody, tfoot);
145       dom.css.setClass(table, options_['css-prefix'] + 'table');
146       container_.appendChild(fragment);
147     }
148   };
149 
150   // Export for closure compiler.
151   this['draw'] = this.draw;
152 
153   /**
154    * Gets active column data.
155    * Useful while handling 'sort' event.
156    * @return {Object} Return active column data.
157    * @example <code>{
158    *   'index': 'number, column index',
159    *   'name': 'string, column name',
160    *   'type': 'string, column type',
161    *   'dir': 'string, column sort direction'
162    * }</code>
163    */
164   this.getColumn = function() {
165     return column_ && {
166       'index': column_.getAttribute('data-index') || column_.cellIndex,
167       'name': column_.name || column_.id || '',
168       'type': column_.getAttribute('data-type') || '',
169       'dir': dom.css.hasClass(
170           column_, options_['css-prefix'] + 'sort-asc') ? 'asc' : 'desc'
171     };
172   };
173 
174   // Export for closure compiler.
175   this['getColumn'] = this.getColumn;
176 
177   /**
178    * Dispatched when any header column is clicked.
179    * @event
180    * @example
181    * table.addEventListener('sort', <b>function</b>() {
182    *   window.console <b>&&</b> console.log(table.getColumn());
183    * });
184    */
185   function sort() {
186     /** @type {string} */ var key = 'sort';
187     if (options_[key] && 'null' != options_[key]) {
188       /** @type {Event} */ var event = arguments[0] || window.event;
189       column_ = /** @type {Node} */ (event.currentTarget || event.srcElement);
190       while ('TH' != column_.nodeName) {
191         // Getting parent, if cell rendered with custom template.
192         column_ = column_.parentNode;
193       }
194       /** @type {string} */ var prefix =
195           /** @type {string} */ (options_['css-prefix']);
196       /** @type {!Array.<string>} */ var css = [key + '-asc', key + '-desc'];
197       var hasClass = dom.css.hasClass(column_, prefix + css[0]);
198       /** @type {string} */ var dir = css[+hasClass];
199 
200       /** @type {NodeList} */ var cells = column_.parentNode.cells;
201       for (/** @type {number} */ var i = 0; i < cells.length;) {
202         /** @type {Element} */ var cell = cells[i++];
203         dom.css.removeClass(cell, prefix + css[0], prefix + css[1]);
204       }
205       dom.css.addClass(column_, prefix + dir);
206       self_.dispatchEvent(key);
207     }
208   }
209 
210   /**
211    * @param {!Array.<Array>} data The data to draw.
212    * @param {Node} thead The thead element.
213    * @param {Node} tbody The tbody element.
214    * @param {Node} tfoot The tfoot element.
215    * @private
216    */
217   function draw_(data, thead, tbody, tfoot) {
218     /** @type {Array.<Object|string>} */ var headers = getHeaders_(data);
219     /** @type {number} */ var length = data.length >>> 0;
220     /** @type {number} */
221     var limit = options_['rows'] && options_['rows']['limit'];
222 
223     for (/** @type {number} */ var i = 0; i < length; i++) {
224       if (limit && limit == i && i < length - 2) {
225         i = length - 2;
226       }
227       /** @type {Array} */ var row = data[i];
228 
229       /** @type {Node} */ var group = !i && thead ? thead : tbody;
230       if (tfoot && i == length - 1) group = tfoot;
231       /** @type {Element} */ var tr = group.insertRow(-1);
232       dom.css.setClass(
233           tr, options_['css-prefix'] + 'row-' + (i % 2 ? 'even' : 'odd'));
234       /** @type {number} */ var index = 0;
235       for (/** @type {number} */ var j = 0; j < row.length; j++) {
236         /** @type {Node} */
237         var cell = tr.appendChild(dom.createElement(i ? 'TD' : 'TH'));
238         /** @type {Object|Date|string|number|boolean} */ var value = row[j];
239         /** @type {Object|string} */ var header = headers[index];
240         /** @type {number} */ var span = getColSpan_(value);
241         cell.colSpan = span;
242         index += span;
243         if (!i && thead) {
244           setHeader_(cell, value);
245           column_ = cell;
246         } else {
247           setValue_(cell, value, header);
248         }
249         setCellClass_(cell, value, index, span, getColSpan_(header));
250 
251         if (header && header['hidden'])
252           cell.style.display = 'none';
253       }
254     }
255   }
256 
257   /**
258    * Sets class to table cells.
259    * @param {Node} cell Table's cell.
260    * @param {Object|Date|string|number|boolean} data The table cell data.
261    * @param {number} index Cell's index.
262    * @param {number} span Cell's span.
263    * @param {number} colSpan Header cell's colspan attribute.
264    * @private
265    */
266   function setCellClass_(cell, data, index, span, colSpan) {
267     /** @type {string} */ var prefix =
268         /** @type {string} */ (options_['css-prefix']);
269     /** @type {Object} */ var sort = /** @type {Object} */ (options_['sort']);
270     /** @type {number} */ var column = !sort['column'] ?
271         sort['column'] + colSpan :
272         sort['column'] + span + colSpan;
273     dom.css.setClass(cell, prefix + 'cell-' + cell.getAttribute('data-type'));
274     if (sort && (index == column) && sort['dir'])
275       dom.css.addClass(cell, prefix + 'sort-' + sort['dir']);
276 
277     if (data['css-class'])
278       dom.css.addClass(cell, data['css-class']);
279   }
280 
281   /**
282    * @param {Array} data The table data.
283    * @return {Array.<Object|string>} Returns table headers.
284    * @private
285    */
286   function getHeaders_(data) {
287     /** @type {Array.<Object|string>} */ var headers = [];
288     /** @type {Array.<Object|string>} */ var row = data[0];
289     for (/** @type {number} */ var i = 0; i < row.length; i++) {
290       /** @type {Object|string} */ var header = row[i];
291       /** @type {number} */ var span = getColSpan_(header);
292       for (/** @type {number} */ var j = 0; j < span; j++) {
293         headers.push(header);
294       }
295     }
296 
297     return headers;
298   }
299 
300   /**
301    * @param {Node} cell The table cell element.
302    * @param {Object|Date|string|number|boolean} data The table cell data.
303    * @private
304    */
305   function setHeader_(cell, data) {
306     if (typeof data != 'object') {
307       data = {'label': data};
308     }
309 
310     if (data != null) {
311       cell.innerHTML = '<span>' + data['label'] + '</span>';
312       for (/** @type {string} */ var key in data) {
313         cell[key] = data[key];
314       }
315       cell.setAttribute('data-type', getType_(data, null));
316       dom.events.addEventListener(cell, dom.events.TYPE.CLICK, sort);
317     }
318   }
319 
320   /**
321    * @param {Node} cell The table cell element.
322    * @param {Object|Date|string|number|boolean} data The table cell data.
323    * @param {Object|string} header The table cell header.
324    * @private
325    */
326   function setValue_(cell, data, header) {
327     if (typeof data != 'object' || data instanceof Date) {
328       data = {'value': data};
329     }
330 
331     if (data != null) {
332       /** @type {Date|string|number|boolean} */ var value = data['value'];
333 
334       if (value != null) {
335         data['type'] = getType_(data, header);
336         if ('date' == data['type'] || value instanceof Date) {
337           data['type'] = 'date';
338           value = formatters.DateFormatter.formatDate(
339               new Date(value),
340               /** @type {string} */ (getFormat_(data, header)));
341         } else if ('bool' == data['type'] || value === !0 || value === !1) {
342           data['type'] = 'bool';
343           value = /** @type {Array} */ (getFormat_(data, header))[+!value];
344         } else if ('number' == data['type']) {
345           value = formatter_.formatNumber(/** @type {number} */ (value));
346         } else {
347           value = template_.parse(
348               /** @type {string} */ (getFormat_(data, header)), data);
349         }
350         cell.innerHTML = value;
351         cell.setAttribute('data-type', data['type']);
352       }
353     }
354   }
355 
356   /**
357    * @param {Object} data The table cell data.
358    * @param {Object|string} header The table cell header.
359    * @return {Array|string} Returns data format.
360    * @private
361    */
362   function getFormat_(data, header) {
363     return /** @type {Array|string} */ (
364         data['format'] ||
365         (header && header['format']) ||
366         options_[data['type'] + '-format']);
367   }
368 
369   /**
370    * @param {Object} data The table cell data.
371    * @param {Object|string} header The table cell header.
372    * @return {string} Returns data type.
373    * @private
374    */
375   function getType_(data, header) {
376     return data['type'] || (header && header['type']) || 'text';
377   }
378 
379   /**
380    * @param {*} data The table cell data.
381    * @return {number} Returns cell column span.
382    * @private
383    */
384   function getColSpan_(data) {
385     return null != data && typeof data == 'object' ? (+data['span'] || 1) : 1;
386   }
387 
388   /**
389    * @type {Element}
390    * @private
391    */
392   var container_ = typeof container == 'string' ?
393       dom.getElementById(container) : container;
394 
395   /**
396    * @type {!formatters.NumberFormatter}
397    * @private
398    */
399   var formatter_ = new formatters.NumberFormatter;
400 
401   /**
402    * @type {!dom.Template}
403    * @private
404    */
405   var template_ = new dom.Template;
406 
407   /**
408    * Reference to last column sorted by.
409    * @type {Node}
410    * @private
411    */
412   var column_ = null;
413 
414   /**
415    * @type {!Object.<string, *>}
416    * @private
417    */
418   var options_ = {};
419 
420   /**
421    * The reference to current class instance. Used in private methods.
422    * @type {!charts.DataTable}
423    * @private
424    */
425   var self_ = this;
426 };
427 
428 
429 // Export for closure compiler.
430 charts['DataTable'] = charts.DataTable;
431