1 2 /** 3 * @fileoverview A simple charts grid implementation. 4 * @version 1.0.0 5 * @see https://google.github.io/styleguide/jsguide.html 6 * @see https://github.com/google/closure-compiler/wiki 7 */ 8 9 10 11 /** 12 * Grid Constructor. 13 * @param {string|Element} container The HTML container. 14 * @requires formatters.NumberFormatter 15 * @constructor 16 */ 17 charts.Grid = function(container) { 18 19 /** 20 * Draws chart grid. 21 * @param {Object=} opt_options A optional grid configuration options. 22 */ 23 this.draw = function(opt_options) { 24 options_ = self_.getOptions(opt_options); 25 if (options_ && 'null' != options_['grid']) { 26 /** @type {number} */ var width = container_.offsetWidth || 200; 27 /** @type {number} */ var height = container_.offsetHeight || width; 28 29 height -= options_['grid']['border'] * 2; 30 drawLines_(drawCanvas_(width, height), width, height); 31 } 32 }; 33 34 /** 35 * Gets grid options merged with defaults grid options. 36 * @param {Object=} opt_options A optional grid configuration options. 37 * @return {!Object.<string, *>} A map of name/value pairs. 38 * @example 39 * options: { 40 * 'grid': {border': 1, 'color': '#ccc', 'lines': 5}, 41 * 'font': {'size': 11, 'family': 'Arial'}, 42 * 'hAxis': true, 43 * 'vAxis': true 44 * } 45 */ 46 this.getOptions = function(opt_options) { 47 opt_options = opt_options || {}; 48 opt_options['grid'] = opt_options['grid'] || {}; 49 opt_options['grid']['border'] = opt_options['grid']['border'] || 1; 50 opt_options['grid']['color'] = opt_options['grid']['color'] || '#ccc'; 51 opt_options['grid']['lines'] = opt_options['grid']['lines'] || 5; 52 opt_options['font'] = opt_options['font'] || {}; 53 opt_options['font']['size'] = opt_options['font']['size'] || 11; 54 opt_options['font']['family'] = opt_options['font']['family'] || 'Arial'; 55 opt_options['hAxis'] = 'hAxis' in opt_options ? opt_options['hAxis'] : true; 56 opt_options['vAxis'] = 'vAxis' in opt_options ? opt_options['vAxis'] : true; 57 opt_options['grid']['direction'] = 58 opt_options['grid']['direction'] || charts.Grid.DIRECTION.BOTTOM_TO_TOP; 59 opt_options['scale'] = 60 'scale' in opt_options ? opt_options['scale'] : false; 61 return opt_options; 62 }; 63 64 /** 65 * @param {Node} canvas The canvas element. 66 * @param {number} width The canvas width. 67 * @param {number} height The canvas height. 68 * @private 69 */ 70 function drawLines_(canvas, width, height) { 71 setTimeout(function() {container_.style.overflow = 'visible';}, 1); 72 if (options_['scale']) scaledLines_(canvas, width, height); 73 else simpleLines_(canvas, width, height); 74 75 if (options_['hAxis'] && options_['data']['columns']) { 76 drawColumnsLabels_(canvas, width, height); 77 } 78 } 79 80 /** 81 * @param {Node} canvas The canvas element. 82 * @param {number} width The canvas width. 83 * @param {number} height The canvas height. 84 * @private 85 */ 86 function scaledLines_(canvas, width, height) { 87 /** @type {number} */ var lines = options_['grid']['lines']; 88 /** @type {string} */ var color = options_['grid']['color']; 89 /** @type {number} */ var padding = options_['padding'] || 0; 90 /** @type {number} */ var border = options_['grid']['border']; 91 /** @type {number} */ var minValue = options_['data']['min']; 92 /** @type {number} */ var maxValue = options_['data']['max']; 93 /** @type {number} */ var delta = maxValue - minValue; 94 95 maxValue = maxValue <= 0 ? 1 : maxValue; 96 delta = delta <= 0 ? 1 : delta; 97 98 /** @type {number} */ 99 var logMinValue = Math.log(minValue) > 0 ? Math.log(minValue) : 0; 100 /** @type {number} */ 101 var logMaxValue = Math.log(maxValue) > 0 ? Math.log(maxValue) : 0; 102 /** @type {number} */ var logDelta = Math.log(delta); 103 /** @type {number} */ 104 var yPadding = options_['radius'] * 2 + (options_['grid']['lines'] - 1) / 2; 105 /** @type {number} */ 106 var lineHeight = (height - padding * 2 - border) / (lines - 1) - border; 107 /** @type {number} */ 108 var minY = Math.ceil((logDelta - (logMaxValue - logMinValue)) * 109 (height - yPadding * 2) / logDelta + yPadding); 110 /** @type {number} */ 111 var maxY = Math.ceil(logDelta * (height - yPadding * 2) / 112 logDelta + yPadding); 113 /** @type {number} */ var deltaY = maxY - minY; 114 /** @type {number} */ var y = height - yPadding; 115 116 for (/** @type {number} */ var i = 0; i < lines + 1; i++) { 117 /** @type {Node} */ var line = createElement_(canvas); 118 line.style.overflow = 'hidden'; 119 if (i && !(!padding && (i == lines || i == 1))) { 120 line.style.borderTop = 'solid ' + border + 'px ' + color; 121 } 122 if (i > 0 && i <= lines) { 123 line.style.height = lineHeight + 'px'; 124 line.style.paddingLeft = '1px'; 125 if (options_['vAxis']) { 126 /** @type {number} */ 127 var dy = deltaY + minY - ((y - yPadding) * deltaY) / 128 (height - yPadding * 2); 129 /** @type {number} */ 130 var row = logDelta + logMinValue - ((dy - yPadding) * 131 logDelta) / (height - yPadding * 2); 132 /** @type {number} */ var value = Math.exp(row); 133 if (row < 1 && minValue <= 0) value = 0; 134 setRowLabel_(line, value); 135 y -= lineHeight; 136 } 137 } else { 138 line.style.height = padding + 'px'; 139 } 140 } 141 } 142 143 /** 144 * @param {Node} canvas The canvas element. 145 * @param {number} width The canvas width. 146 * @param {number} height The canvas height. 147 * @private 148 */ 149 function simpleLines_(canvas, width, height) { 150 /** @type {number} */ var size = getRowSize_(width, height); 151 /** @type {number} */ var length = options_['grid']['lines'] - 1; 152 /** @type {number} */ 153 var delta = options_['data']['max'] / length; 154 /** @type {string} */ 155 var direction = 156 charts.Grid.DIRECTION.TOP_TO_BOTTOM == options_['direction'] || 157 charts.Grid.DIRECTION.BOTTOM_TO_TOP == options_['direction'] ? 158 'Top' : 'Left'; 159 for (/** @type {number} */ var index = 0; index <= length; index++) { 160 /** @type {Node} */ var line = createLine_(canvas, width, height); 161 line.style[direction.toLowerCase()] = (size * index) + 'px'; 162 if (index && index < length) { 163 // Don't set border for first and last line. 164 line.style['border' + direction] = 'solid ' + 165 options_['grid']['border'] + 'px ' + options_['grid']['color']; 166 } 167 168 if (options_['vAxis']) { 169 //setRowLabel_(line, delta / length * (length - index)); 170 setRowLabel_(line, delta * (length - index)); 171 } 172 } 173 } 174 175 /** 176 * @param {Node} canvas The canvas element. 177 * @param {number} width The canvas width. 178 * @param {number} height The canvas height. 179 * @return {Node} Returns create grid line element. 180 * @private 181 */ 182 function createLine_(canvas, width, height) { 183 /** @type {number} */ var direction = options_['direction']; 184 /** @type {number} */ var border = options_['grid']['border']; 185 /** @type {number} */ var size = getRowSize_(width, height); 186 /** @type {Node} */ var line; 187 188 if (charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction || 189 charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction) { 190 line = createElement_(canvas, width - border, size); 191 } else if (charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction || 192 charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) { 193 line = createElement_(canvas, size - border, height); 194 } 195 196 return line; 197 } 198 199 /** 200 * @param {number} width The canvas width. 201 * @param {number} height The canvas height. 202 * @return {number} Returns line size. 203 * @private 204 */ 205 function getRowSize_(width, height) { 206 /** @type {number} */ var direction = options_['direction']; 207 /** @type {number} */ var border = options_['grid']['border']; 208 /** @type {number} */ var size = ( 209 charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction || 210 charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction) ? height : width; 211 212 return (size - border) / (options_['grid']['lines'] - 1); 213 } 214 215 /** 216 * @param {Node} canvas The canvas element. 217 * @param {number} width The canvas width. 218 * @param {number} height The canvas height. 219 * @private 220 */ 221 function drawColumnsLabels_(canvas, width, height) { 222 /** @type {Array.<string>} */ var columns = options_['data']['columns']; 223 /** @type {number} */ var length = columns.length; 224 /** @type {number} */ var padding = (options_['padding'] || 1) * 2; 225 /** @type {number} */ var x = (width - padding * 2) / (length - 2); 226 /** @type {number} */ var fontSize = options_['font']['size'] || 13; 227 /** @type {string} */ 228 var label = getColumnLabel_(columns[columns.length - 1]); 229 /** @type {number} */ 230 var symbolWidth = label ? label.length * (fontSize - fontSize / 3) : 1; 231 /** @type {number} */ 232 var maxAmount = Math.round((width - padding * 2) / symbolWidth); 233 234 for (/** @type {number} */ var i = 1; i < length;) { 235 /** @type {Node} */ var div; 236 if (length > 2) { 237 div = createElement_(canvas, null, null, height + 5, 238 (padding + x * (i - 1) - symbolWidth / 2)); 239 } else { 240 div = createElement_(canvas, null, null, height + 5, 241 (width / 2 - padding * 2)); 242 } 243 244 setColumnLabel_(div, columns[i]); 245 i += Math.round(Math.max(length / Math.round(maxAmount), 1)); 246 } 247 } 248 249 /** 250 * @param {*} column The column data. 251 * @return {string} Returns formatted column label. 252 * @private 253 */ 254 function getColumnLabel_(column) { 255 if (column instanceof Date) { 256 column = formatters.DateFormatter.formatDate( 257 /** @type {Date} */(column), 'YYYY-MM-dd'); 258 } 259 return '' + column; 260 } 261 262 /** 263 * @param {Node} column The column element. 264 * @param {*} label The column label text. 265 * @see setRowLabel_(row, label) 266 * @private 267 */ 268 function setColumnLabel_(column, label) { 269 column.style.position = 'absolute'; 270 column.style.whiteSpace = 'nowrap'; 271 column.innerHTML = getColumnLabel_(label); 272 } 273 274 /** 275 * @param {Node} row The row element. 276 * @param {number} label The row label text. 277 * @private 278 */ 279 function setRowLabel_(row, label) { 280 /** @type {number} */ var width = row.offsetWidth; 281 /** @type {number} */ var direction = options_['direction']; 282 /** @type {number} */ var font = options_['font']['size']; 283 /** @type {string} */ var css = 'overflow:hidden;position:absolute;'; 284 285 if (charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction || 286 charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction) { 287 label = options_['data']['max'] - label; 288 } else if (charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction || 289 charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) { 290 label = label; 291 } 292 293 if (charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction || 294 charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) { 295 css += 'margin-left:-' + (width / 2) + 'px;bottom:0;' + 296 'width:' + width + 'px;' + 297 'text-align:center;margin-bottom:-' + (font + font / 2) + 'px;'; 298 } else { 299 width = 50; 300 css += 'left:-' + (width + 5) + 'px;width:' + width + 'px;' + 301 'text-align:right;margin-top:-' + (font / 2) + 'px;'; 302 } 303 row.innerHTML = '<div style="' + css + '">' + 304 formatter_.roundNumber(label) + '</div>'; 305 } 306 307 /** 308 * @param {number} width The canvas width. 309 * @param {number} height The canvas height. 310 * @return {Node} Returns canvas element. 311 * @private 312 */ 313 function drawCanvas_(width, height) { 314 /** @type {number} */ var border = options_['grid']['border']; 315 /** @type {string} */ var color = options_['grid']['color']; 316 /** @type {Node} */ 317 var canvas = createElement_(container_, width - border * 2, height); 318 canvas.style.border = 'solid ' + border + 'px ' + color; 319 canvas.style.fontFamily = options_['font']['family']; 320 canvas.style.fontSize = options_['font']['size'] + 'px'; 321 canvas.style.color = color; 322 return canvas; 323 } 324 325 /** 326 * @param {Node} parent The parent element. 327 * @param {?number=} opt_width The optional width of new element. 328 * @param {?number=} opt_height The optional height of new element. 329 * @param {?number=} opt_top The optional Y of new element. 330 * @param {?number=} opt_left The optional X of new element. 331 * @return {Node} Returns created element. 332 * @private 333 */ 334 function createElement_(parent, opt_width, opt_height, opt_top, opt_left) { 335 /** @type {Node} */ var node = parent.appendChild(dom.createElement('DIV')); 336 node.style.position = 'absolute'; 337 if (opt_width) node.style.width = opt_width + 'px'; 338 if (opt_height) node.style.height = opt_height + 'px'; 339 if (opt_top) node.style.top = opt_top + 'px'; 340 if (opt_left) node.style.left = opt_left + 'px'; 341 return node; 342 } 343 344 /** 345 * The reference to current class instance. Used in private methods. 346 * @type {!charts.Grid} 347 * @private 348 */ 349 var self_ = this; 350 351 /** 352 * The reference to HTML chart container. 353 * @type {Element} 354 * @private 355 */ 356 var container_ = typeof container == 'string' ? 357 dom.getElementById(container) : container; 358 359 /** 360 * @dict 361 * @private 362 */ 363 var options_ = null; 364 365 /** 366 * @type {!formatters.NumberFormatter} 367 * @private 368 */ 369 var formatter_ = new formatters.NumberFormatter; 370 }; 371 372 373 /** 374 * Enum of grid direction. 375 * @enum {number} 376 * @static 377 * @example 378 * <code>{ LEFT_TO_RIGHT, RIGHT_TO_LEFT, BOTTOM_TO_TOP, TOP_TO_BOTTOM }</code> 379 */ 380 charts.Grid.DIRECTION = { 381 LEFT_TO_RIGHT: 1, // Standard bar chart. 382 RIGHT_TO_LEFT: 2, // Flipped bar chart. 383 BOTTOM_TO_TOP: 3, // Column chart. 384 TOP_TO_BOTTOM: 4 // Flipped column chart. 385 }; 386