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