1 2 /** 3 * @fileoverview Simple bar chart 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 * BarChart constructor. 13 * @param {string|Element} container The HTML container. 14 * @constructor 15 * @class Simple horizontal bar chart implementation. 16 * @extends {charts.BaseChart} charts.BaseChart 17 * @requires animation 18 * @requires charts.Grid 19 * @requires formatters.NumberFormatter 20 * @example 21 * <b>var</b> chart = <b>new</b> charts.BarChart('container_id'); 22 * chart.draw([['Year', 'Sales', 'Expenses', 'Profit'], 23 * [2011, 80, 30, 45], [2012, 65, 130, 90], [2013, 45, 100, 60]]); 24 * 25 * <div style="border: solid 1px #ccc; margin: 5px; padding: 5px; width: 560px"> 26 * <div id="chart-container" 27 * style="width: 560px; height: 200px;"></div> 28 * </div> 29 * <script src="https://greylock.js.org/greylock.js"></script> 30 * <script> 31 * var chart = new charts.BarChart('chart-container'); 32 * chart.draw([['Year', 'Sales', 'Expenses', 'Profit'], 33 * [2011, 80, 30, 45], [2012, 65, 130, 90], [2013, 45, 100, 60]]); 34 * </script> 35 */ 36 charts.BarChart = function(container) { 37 charts.BaseChart.apply(this, arguments); 38 39 /** 40 * Draws the chart based on <code>data</code> and <code>opt_options</code>. 41 * @param {!Array.<Array>} data A chart data. 42 * @param {Object=} opt_options A optional chart's configuration options. 43 * @override 44 * @see charts.BaseChart#getOptions 45 * @example 46 * options: { 47 * 'stroke': 1, 48 * 'font': {'size': 11}, 49 * 'flip': <b>false</b> 50 * } 51 */ 52 this.draw = function(data, opt_options) { 53 data_ = data; 54 options_ = getOptions_(opt_options); 55 options_['direction'] = getDirection_(); 56 self_.tooltip.setOptions(options_); 57 58 /** @type {string} */ var content = ''; 59 /** @type {!Array.<Array>} */ var rows = self_.getDataRows(data); 60 /** @type {!Array.<string>} */ var columns = self_.getDataColumns(data); 61 /** @type {!Array.<number>} */ var range = self_.getDataRange(data, 1); 62 63 /** @type {number} */ var width = self_.container.offsetWidth || 200; 64 /** @type {number} */ var height = self_.container.offsetHeight || width; 65 /** @type {number} */ 66 var border = /** @type {number} */ (options_['stroke']); 67 68 bars_ = 1; // Reset bars counter for redrawing issue. 69 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 70 bars_ += rows[i].length; 71 } 72 73 barWidth_ = width / bars_ - border * 2; 74 barHeight_ = height / bars_ - border * 2; 75 76 bars_ = 0; 77 for (i = 0; i < rows.length; i++) { 78 /** @type {Array} */ var row = rows[i]; 79 /** @type {number} */ var j = 1; 80 for (; j < row.length; j++) { 81 /** @type {string} */ var tooltip = self_.tooltip.parse( 82 row[0], columns[j], formatter_.formatNumber(row[j])); 83 84 content += getBarContent_(getBarRect_(rows, i, j, range), 85 options_['colors'][j - 1], tooltip); 86 bars_++; 87 } 88 89 bars_++; // Add space between bar groups. 90 } 91 92 initGrid_(range); 93 self_.drawContent(content); 94 initEvents_(); 95 }; 96 97 // Export for closure compiler. 98 this['draw'] = this.draw; 99 100 /** 101 * Draws content into <code>this.container</code> as <code>innerHTML</code>. 102 * @param {string} content The HTML markup content. 103 * @param {number=} opt_width Optional chart width. 104 * @param {number=} opt_height Optional chart height. 105 * @override 106 */ 107 this.drawContent = function(content, opt_width, opt_height) { 108 opt_width = opt_width || self_.container.offsetWidth || 200; 109 opt_height = opt_height || self_.container.offsetHeight || opt_width; 110 111 self_.container.style.position = 'relative'; 112 self_.container.style.overflow = 'hidden'; 113 self_.container.innerHTML += 114 '<div style="position:absolute;width:' + opt_width + 115 'px;height:' + opt_height + 'px">' + content + '</div>'; 116 }; 117 118 /** 119 * @param {!Array.<number>} range The data range with min and max values. 120 * @private 121 */ 122 function initGrid_(range) { 123 /** @type {number} */ var minValue = range[0]; 124 /** @type {number} */ var maxValue = range[1]; 125 /** @type {!Array.<string>} */ var columns = []; 126 //for (/** @type {number} */ var i = 0; i < data_.length; i++) { 127 //columns.push(''); 128 //columns.push(data_[i][0]); 129 //} 130 /** @type {!charts.Grid} */ var grid = new charts.Grid(self_.container); 131 options_['data'] = {'min': minValue, 'max': maxValue, 'columns': columns}; 132 options_['padding'] = 0; 133 grid.draw(options_); 134 } 135 136 /** 137 * Gets chart's direction. 138 * @return {number} Returns chart direction. 139 * @see charts.BarChart#DIRECTION 140 * @private 141 */ 142 function getDirection_() { 143 /** @type {number} */ var direction = options_['direction'] || 144 charts.Grid.DIRECTION.LEFT_TO_RIGHT; 145 if (options_['flip']) { 146 if (direction == charts.Grid.DIRECTION.LEFT_TO_RIGHT) 147 direction = charts.Grid.DIRECTION.RIGHT_TO_LEFT; 148 else if (direction == charts.Grid.DIRECTION.BOTTOM_TO_TOP) 149 direction = charts.Grid.DIRECTION.TOP_TO_BOTTOM; 150 } 151 return direction; 152 } 153 154 /** 155 * @param {!Array.<Array>} rows Chart data rows. 156 * @param {number} rowIndex Current row index. 157 * @param {number} columnIndex Current column index. 158 * @param {!Array.<number>} range The data range with min and max values. 159 * @return {!Object.<string, number>} Returns rect as {x, y, width, height}. 160 * @private 161 */ 162 function getBarRect_(rows, rowIndex, columnIndex, range) { 163 /** @type {!Object.<string, number>} */ 164 var rect = {x: 0, y: 0, width: 0, height: 0}; 165 166 /** @type {number} */ var width = self_.container.offsetWidth || 200; 167 /** @type {number} */ var height = self_.container.offsetHeight || width; 168 /** @type {number} */ 169 var border = /** @type {number} */ (options_['stroke']); 170 171 /** @type {number} */ var minValue = range[0]; 172 /** @type {number} */ var maxValue = range[1]; 173 174 /** @type {Array} */ var row = rows[rowIndex]; 175 /** @type {number} */ var value = row[columnIndex]; 176 /** @type {number} */ var direction = getDirection_(); 177 178 if (direction < charts.Grid.DIRECTION.BOTTOM_TO_TOP) { 179 // Horizontal charts. 180 rect.width = value / maxValue * (width - border * 4); 181 // rect.height = (height / row.length / rows.length) - border * 2; 182 rect.height = barHeight_; 183 rect.x = border; 184 rect.y = (rect.height + border * 2) * bars_ + (rect.height + border * 2); 185 //if (charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) 186 // rect.x = width - rect.width - border; 187 } else if (direction >= charts.Grid.DIRECTION.BOTTOM_TO_TOP) { 188 // Vertical charts. 189 // rect.width = (width / row.length / rows.length) - border * 2; 190 rect.width = barWidth_; 191 rect.height = value / maxValue * (height - border * 4); 192 rect.x = (rect.width + border * 2) * bars_ + (rect.width + border * 2); 193 rect.y = border; 194 //if (charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction) 195 // rect.y = height - rect.height; 196 } 197 198 return rect; 199 } 200 201 /** 202 * Gets chart's options merged with defaults chart's options. 203 * @param {Object=} opt_options A optional chart's configuration options. 204 * @return {!Object.<string, *>} A map of name/value pairs. 205 * @see charts.BaseChart#getOptions 206 * @private 207 */ 208 function getOptions_(opt_options) { 209 opt_options = opt_options || {}; 210 opt_options['stroke'] = opt_options['stroke'] || 1; 211 opt_options['font'] = opt_options['font'] || {}; 212 opt_options['font']['size'] = opt_options['font']['size'] || 11; 213 return self_.getOptions(opt_options); 214 } 215 216 /** 217 * @param {!Object.<string, number>} rect Bar rect. 218 * @param {string} color Bar color. 219 * @param {string} tooltip Bar tooltip. 220 * @return {string} Returns bar content as HTML markup string. 221 * @private 222 */ 223 function getBarContent_(rect, color, tooltip) { 224 return '<div class="bar" style="' + 225 'width:' + rect.width + 'px;' + 226 'height:' + rect.height + 'px;' + 227 'top:' + rect.y + 'px;' + 228 'left:' + rect.x + 'px;' + 229 'background:' + color + ';' + 230 'border:solid ' + options_['stroke'] + 'px #fff;' + 231 'color:#fff;' + 232 'overflow:hidden;' + 233 'position:absolute;' + 234 'opacity:' + options_['opacity'] + ';' + 235 'filter: alpha(opacity=' + (options_['opacity'] * 100) + ');' + 236 'font-family:' + options_['font']['family'] + ';' + 237 'font-size:' + options_['font']['size'] + 'px;' + 238 '" tooltip="' + tooltip + '"></div>'; 239 } 240 241 /** 242 * Initializes events handlers. 243 * @private 244 */ 245 function initEvents_() { 246 /** @type {!Array|NodeList} */ 247 var nodes = dom.getElementsByClassName(self_.container, 'bar'); 248 for (/** @type {number} */ var i = 0; i < nodes.length; i++) { 249 setEvents_(nodes[i]); 250 } 251 } 252 253 /** 254 * Sets events handlers. 255 * @param {!Element} node The element. 256 * @private 257 */ 258 function setEvents_(node) { 259 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOVER, function(e) { 260 node.style.opacity = 1; 261 node.style.filter = 'alpha(opacity=100)'; 262 263 self_.tooltip.show(e); 264 /** @type {!Object.<string, number>} */ 265 var position = getTooltipPosition_(node); 266 self_.tooltip.show(e, position.x, position.y); 267 }); 268 269 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOUT, function(e) { 270 node.style.opacity = options_['opacity']; 271 node.style.filter = 'alpha(opacity=' + (options_['opacity'] * 100) + ')'; 272 self_.tooltip.hide(e); 273 }); 274 275 dom.events.dispatchEvent(node, dom.events.TYPE.MOUSEOUT); 276 initAnimation_(node); 277 } 278 279 /** 280 * Initializes bar animation. 281 * @param {!Element|Node} node The element. 282 * @private 283 */ 284 function initAnimation_(node) { 285 /** @type {number} */ 286 var width = parseFloat(node.style.width) || node.offsetWidth; 287 /** @type {number} */ 288 var height = parseFloat(node.style.height) || node.offsetHeight; 289 /** @type {number} */ 290 var x = parseFloat(node.style.left) || node.offsetLeft; 291 /** @type {number} */ 292 var y = parseFloat(node.style.top) || node.offsetTop; 293 /** @type {number} */ var direction = getDirection_(); 294 295 if (direction == charts.Grid.DIRECTION.LEFT_TO_RIGHT) { 296 node.style.width = '1px'; 297 animation.animate(node, {'width': width}); 298 } else if (direction == charts.Grid.DIRECTION.RIGHT_TO_LEFT) { 299 node.style.width = '1px'; 300 node.style.right = '1px'; 301 node.style.left = 'auto'; 302 animation.animate(node, {'width': width}); 303 } else if (direction == charts.Grid.DIRECTION.TOP_TO_BOTTOM) { 304 node.style.height = '1px'; 305 animation.animate(node, {'height': height}); 306 } else if (direction == charts.Grid.DIRECTION.BOTTOM_TO_TOP) { 307 node.style.height = '1px'; 308 node.style.bottom = '1px'; 309 node.style.top = 'auto'; 310 animation.animate(node, {'height': height}); 311 } 312 } 313 314 /** 315 * @param {!Element|Node} node The element. 316 * @return {!Object.<string, number>} Returns tooltip position as {x, y}. 317 * @private 318 */ 319 function getTooltipPosition_(node) { 320 /** @type {number} */ var width = node.offsetWidth; 321 /** @type {number} */ var height = node.offsetHeight; 322 /** @type {!Object.<string, number>} */ 323 var result = {x: node.offsetLeft, y: node.offsetTop}; 324 325 while (node = node.offsetParent) { 326 result.x += node.offsetLeft; 327 result.y += node.offsetTop; 328 } 329 330 /** @type {Node} */ var tooltip = self_.tooltip.getTooltipElement(); 331 /** @type {number} */ var direction = getDirection_(); 332 if (direction == charts.Grid.DIRECTION.LEFT_TO_RIGHT) { 333 result.x += width; 334 result.y += options_['stroke']; 335 } else if (direction == charts.Grid.DIRECTION.RIGHT_TO_LEFT) { 336 result.x -= tooltip.offsetWidth; 337 result.y += options_['stroke']; 338 } else if (direction == charts.Grid.DIRECTION.BOTTOM_TO_TOP) { 339 result.x += options_['stroke']; 340 result.y -= tooltip.offsetHeight; 341 } else if (direction == charts.Grid.DIRECTION.TOP_TO_BOTTOM) { 342 result.x += options_['stroke']; 343 result.y += height; 344 } 345 return result; 346 } 347 348 /** 349 * The reference to current class instance. Used in private methods. 350 * @type {!charts.BarChart} 351 * @private 352 */ 353 var self_ = this; 354 355 /** 356 * @type {Array.<Array>} 357 * @private 358 */ 359 var data_ = null; 360 361 /** 362 * @dict 363 * @private 364 */ 365 var options_ = null; 366 367 /** 368 * Total bars counter. 369 * @type {number} 370 * @private 371 */ 372 var bars_ = 1; 373 374 /** 375 * Vertical charts bar width. 376 * @type {number} 377 * @private 378 */ 379 var barWidth_ = 0; 380 381 /** 382 * Horizontal charts bar height. 383 * @type {number} 384 * @private 385 */ 386 var barHeight_ = 0; 387 388 /** 389 * @type {!formatters.NumberFormatter} 390 * @private 391 */ 392 var formatter_ = new formatters.NumberFormatter; 393 }; 394 395 // Export for closure compiler. 396 charts['BarChart'] = charts.BarChart; 397