1 2 /** 3 * @fileoverview Simple line 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 * LineChart constructor. 13 * @param {string|Element} container The HTML container. 14 * @constructor 15 * @extends {charts.BaseChart} charts.BaseChart 16 * @requires charts.Grid 17 * @requires formatters.NumberFormatter 18 * @example 19 * <b>var</b> chart = <b>new</b> charts.LineChart('container_id'); 20 * chart.draw([['Year', 'Sales', 'Expenses', 'Profit'], 21 * [2008, 10, 65, 90], [2009, 165, 30, 60], [2010, 85, 150, 20], 22 * [2011, 80, 60, 45], [2012, 65, 130, 90], [2013, 45, 100, 60]]); 23 * 24 * <div id="chart-container" 25 * style="width: 560px; height: 200px; margin: 0 0 20px 20px;"></div> 26 * <script src="https://greylock.js.org/greylock.js"></script> 27 * <script> 28 * var chart = new charts.LineChart('chart-container'); 29 * chart.draw([['Year', 'Sales', 'Expenses', 'Profit'], 30 * [2008, 10, 65, 90], [2009, 165, 30, 60], [2010, 85, 150, 20], 31 * [2011, 80, 60, 45], [2012, 65, 130, 90], [2013, 45, 100, 60]]); 32 * </script> 33 */ 34 charts.LineChart = function(container) { 35 charts.BaseChart.apply(this, arguments); 36 37 /** 38 * Draws the chart based on <code>data</code> and <code>opt_options</code>. 39 * @param {!Array.<Array>} data A chart data. 40 * @param {Object=} opt_options A optional chart's configuration options. 41 * @override 42 * @example 43 * options: { 44 * 'stroke': 2, 45 * 'radius': 4, 46 * 'opacity': 0.4, 47 * 'font': {'size': 11} 48 * } 49 */ 50 this.draw = function(data, opt_options) { 51 data_ = prepareData_(data); 52 /** @type {!charts.Grid} */ var grid = new charts.Grid(self_.container); 53 options_ = grid.getOptions(getOptions_(opt_options)); 54 formatter_ = new formatters.NumberFormatter( 55 /** @type {Object.<string,*>} */(options_['formatter'])); 56 self_.tooltip.setOptions(options_); 57 58 /** @type {number} */ var width = self_.container.offsetWidth || 200; 59 /** @type {number} */ var height = self_.container.offsetHeight || width; 60 /** @type {number} */ 61 var radius = /** @type {number} */(options_['radius']); 62 /** @type {string} */ var content = ''; 63 /** @type {!Array.<Array>} */ var rows = self_.getDataRows(data_); 64 /** @type {!Array.<string>} */ var columns = self_.getDataColumns(data_); 65 /** @type {!Object.<number>} */ var scaledElements = 66 chartElementsScaling_(radius, columns.length, width); 67 radius = scaledElements['radius']; 68 options_['stroke'] = scaledElements['line']; 69 /** @type {Array.<number>} */ var range = self_.getDataRange(data_, 1); 70 /** @type {number} */ var maxValue = range[1]; 71 /** @type {number} */ var minValue = range[0]; 72 if (maxValue == minValue) { 73 minValue = minValue - (options_['grid']['lines'] - 1) / 2; 74 maxValue = maxValue + (options_['grid']['lines'] - 1) / 2; 75 } 76 /** @type {Array<Array>} */ var points = []; 77 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 78 /** @type {Array.<number>} */ var row = rows[i]; 79 /** @type {string} */ var color = options_['colors'][i]; 80 81 // TODO (alex): Use padding for minValue and maxValue. 82 /** @type {number} */ var xAxis = (width - radius * 2) / row.length; 83 /** @type {number} */ var yAxis = (height - radius * 2) / maxValue; 84 85 points = getPoints_(row, width, height, minValue, maxValue); 86 /** @type {Array.<string>} */ 87 var tooltips = getPointsTooltips_(points, rows, columns); 88 89 options_['smooth'] = points.length > 1; 90 content += charts.IS_SVG_SUPPORTED ? 91 getSvgContent_(points, color, radius, tooltips, width, height) : 92 getVmlContent_(points, color, radius, tooltips); 93 } 94 95 options_['data'] = {'min': minValue, 'max': maxValue, 'columns': columns}; 96 //options_['padding'] = radius * 2; 97 options_['direction'] = charts.Grid.DIRECTION.BOTTOM_TO_TOP; 98 grid.draw(options_); 99 100 self_.drawContent(content, width, height); 101 initEvents_(); 102 }; 103 104 // Export for closure compiler. 105 this['draw'] = this.draw; 106 107 /** 108 * Calculates dot radius and line width. 109 * @param {number} radius Default dot radius. 110 * @param {number} length Columns length. 111 * @param {number} width Container width. 112 * @return {!Object.<number>} Scaled radius and line width. 113 * @private 114 */ 115 function chartElementsScaling_(radius, length, width) { 116 length = length - 1; // skip first column. 117 /** @type {number} */ 118 var stroke = /** @type {number} */(options_['stroke']); 119 /** @type {number} */ var radiusScale = width / (length * radius * 2); 120 /** @type {number} */ var lineScale = width / (length * stroke * 4); 121 lineScale = lineScale > 1 ? 1 : lineScale < 0.35 ? 0.35 : lineScale; 122 radiusScale = radiusScale > 1 ? 1 : radiusScale < 0.35 ? 0.35 : radiusScale; 123 return { 124 'radius': radius * radiusScale, 125 'line': stroke * lineScale 126 }; 127 } 128 129 /** 130 * @param {Array.<Array>} points The line points. 131 * @param {Array.<Array>} rows The data rows. 132 * @param {Array.<string>} columns The data columns. 133 * @return {Array.<string>} Returns tooltip for each point. 134 * @private 135 */ 136 function getPointsTooltips_(points, rows, columns) { 137 /** @type {Array.<string>} */ var tooltips = []; 138 /** @type {number} */ var fontSize = options_['font']['size']; 139 for (/** @type {number} */ var i = 0; i < points.length; i++) { 140 var tip = '<b>' + columns[i + 1] + '</b>'; 141 for (/** @type {number} */ var j = 0; j < rows.length; j++) { 142 tip += '<br><span style=\'' + 143 'background-color: ' + options_['colors'][j] + ';' + 144 'display: inline-block;' + 145 'width: ' + (options_['radius'] * 2) + 'px;' + 146 'height: ' + (options_['radius'] * 2) + 'px;' + 147 'border-radius:' + options_['radius'] + 'px;\'>' + 148 '</span> ' + rows[j][0] + 149 ': ' + formatter_.formatNumber(rows[j][i + 1]); 150 } 151 tooltips.push(tip); 152 } 153 return tooltips; 154 } 155 156 /** 157 * Prepares data for drawing. 158 * @param {Array.<Array>} data Raw chart data. 159 * @return {Array.<Array>} Chart data. 160 * @private 161 */ 162 function prepareData_(data) { 163 /** @type {Array.<Array>} */ var result = []; 164 for (/** @type {number} */ var i = 0; i < data.length; i++) { 165 /** @type {Array} */ var row = data[i]; 166 for (/** @type {number} */ var j = 0; j < row.length; j++) { 167 if (!result[j]) result[j] = []; 168 result[j][i] = row[j]; 169 } 170 } 171 return result; 172 } 173 174 /** 175 * Gets chart's options merged with defaults chart's options. 176 * @param {Object=} opt_options A optional chart's configuration options. 177 * @return {!Object.<string, *>} A map of name/value pairs. 178 * @see charts.BaseChart#getOptions 179 * @private 180 * @example 181 * options: { 182 * 'stroke': 2, 183 * 'radius': 4, 184 * 'opacity': 0.4, 185 * 'font': {'size': 11} 186 * } 187 */ 188 function getOptions_(opt_options) { 189 opt_options = opt_options || {}; 190 opt_options['stroke'] = opt_options['stroke'] || 2; 191 opt_options['radius'] = opt_options['radius'] || 4; 192 opt_options['opacity'] = opt_options['opacity'] || 0.4; 193 opt_options['font'] = opt_options['font'] || {}; 194 opt_options['font']['size'] = opt_options['font']['size'] || 11; 195 opt_options['smooth'] = opt_options['smooth'] || false; 196 opt_options['formatter'] = opt_options['formatter'] || {}; 197 opt_options['anim'] = (opt_options['anim'] || opt_options['smooth']) && 198 !window['VBArray']; 199 opt_options['duration'] = opt_options['duration'] || 0.7; 200 return self_.getOptions(opt_options); 201 } 202 203 /** 204 * Initializes events handlers. 205 * @private 206 */ 207 function initEvents_() { 208 /** @type {string} */ 209 var tagName = charts.IS_SVG_SUPPORTED ? 'circle' : 'oval'; 210 /** @type {NodeList} */ 211 var nodes = dom.getElementsByTagName(self_.container, tagName); 212 /** @type {number} */ var length = nodes.length; 213 /** @type {Object} */ var columns = options_['data']['columns']; 214 /** @type {number} */ var cols = columns.length - 1; 215 for (/** @type {number} */ var i = 0; i < length; i++) { 216 setEvents_(nodes, nodes[i], length, cols); 217 } 218 219 if (charts.IS_SVG_SUPPORTED) { 220 var root = dom.getElementsByTagName(self_.container, 'svg')[0]; 221 root.style.paddingBottom = (options_['radius'] / 2) + 'px'; 222 } 223 } 224 225 /** 226 * Sets events handlers. 227 * @param {NodeList} nodes Points. 228 * @param {!Element} node The element. 229 * @param {number} length The number of points. 230 * @param {number} columns The number of columns. 231 * @private 232 */ 233 function setEvents_(nodes, node, length, columns) { 234 /** @type {string} */ 235 var attr = charts.IS_SVG_SUPPORTED ? 'stroke-opacity' : 'opacity'; 236 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOVER, function(e) { 237 /** @type {number} */ 238 var point = +node.getAttribute('column') - 1; 239 for (; point < length; point += columns) { 240 if (charts.IS_SVG_SUPPORTED) { 241 nodes[point].setAttribute(attr, options_['opacity']); 242 } else { 243 // Note: node.firstChild is <vml:stroke> element. 244 nodes[point].firstChild[attr] = options_['opacity']; 245 } 246 } 247 self_.tooltip.show(e); 248 }); 249 250 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOUT, function(e) { 251 /** @type {number} */ 252 var point = +node.getAttribute('column') - 1; 253 for (; point < length; point += columns) { 254 if (charts.IS_SVG_SUPPORTED) { 255 nodes[point].setAttribute(attr, 0); 256 } else { 257 // Note: node.firstChild is <vml:stroke> element. 258 nodes[point].firstChild[attr] = 0; 259 } 260 } 261 self_.tooltip.hide(e); 262 }); 263 264 dom.events.dispatchEvent(node, dom.events.TYPE.MOUSEOUT); 265 } 266 267 /** 268 * @param {Array.<Array>} points The line points. 269 * @param {string} stroke The stroke color. 270 * @param {number} radius The dot radius. 271 * @param {Array.<string>} tooltips The points tooltips. 272 * @param {number} width Container width. 273 * @param {number} height Container height. 274 * @return {string} Returns SVG markup string. 275 * @private 276 */ 277 function getSvgContent_(points, stroke, radius, tooltips, width, height) { 278 /** @type {Array.<string>} */ var dots = []; 279 /** @type {number} */ var size = options_['smooth'] && 280 radius > options_['opacity'] + 3 ? options_['opacity'] + 3 : radius; 281 /** @type {number} */ var duration = options_['duration']; 282 /** @type {number} */ var length = points.length; 283 for (/** @type {number} */ var i = 0; i < length; i++) { 284 /** @type {Array.<number>} */ var point = points[i]; 285 286 dots.push('<circle cx="' + point[0] + '" cy="' + (options_['anim'] ? 287 height : point[1]) + '" r="' + size + '" fill="' + stroke + '" ' + 288 'column="' + (i + 1) + '" tooltip="' + tooltips[i] + '" ' + 289 'stroke="' + stroke + '" stroke-opacity="' + 290 options_['opacity'] + '" stroke-width="' + radius + '">' + 291 (options_['anim'] && 292 293 '<animate attributeName="cy" from="' + height + '" to="' + 294 point[1] + '" dur="' + duration + 's" ' + 295 'fill="freeze"></animate>') + '</circle>'); 296 } 297 298 return (options_['anim'] ? calcPath_(points, stroke, width, height) : 299 '<polyline style="fill:none;stroke:' + stroke + ';stroke-width:' + 300 options_['stroke'] + '" ' + 'points="' + points.join(' ') + '"/>') + 301 dots.join(''); 302 } 303 304 /** 305 * @param {Array.<Array>} points The line points. 306 * @param {string} stroke The stroke color. 307 * @param {number} width Container width. 308 * @param {number} height Container height. 309 * @return {string} Path. 310 * @private 311 */ 312 function calcPath_(points, stroke, width, height) { 313 /** @type {string} */ var path = '<path fill="none" stroke="' + stroke + 314 '" stroke-width="' + options_['stroke'] + '" '; 315 /** @type {number} */ var duration = options_['duration']; 316 /** @type {number} */ var radius = width / points.length / 2; 317 /** @type {string} */ var endcords; 318 /** @type {string} */ var startCords; 319 /** @type {string} */ var result = path; 320 /** @type {number} */ var length = points.length; 321 for (/** @type {number} */ var i = 0; i < length; i++) { 322 if (i + 1 < length) { 323 /** @type {number} */ var currX = points[i][0]; 324 /** @type {number} */ var currY = points[i][1]; 325 /** @type {number} */ var nextX = points[i + 1][0]; 326 /** @type {number} */ var nextY = points[i + 1][1]; 327 endcords = ('M' + currX + ',' + currY + 328 ' C' + (currX + radius) + ',' + currY + 329 ' ' + (nextX - radius) + ',' + nextY + 330 ' ' + nextX + ',' + nextY); 331 startCords = ('M' + currX + ',' + height + 332 ' C' + (currX + radius) + ',' + height + 333 ' ' + (nextX - radius) + ',' + height + 334 ' ' + nextX + ',' + height); 335 336 result += 'd="' + startCords + '">' + (options_['anim'] && 337 '<animate attributeName="d" from="' + startCords + 338 '" to="' + endcords + '" dur="' + duration + 's" ' + 339 'fill="freeze"></animate>') + '</path>' + 340 (i < length - 2 ? path : ''); 341 } 342 } 343 return 1 == length ? '' : result; 344 } 345 346 /** 347 * @param {Array.<Array>} points The line points. 348 * @param {string} stroke The stroke color. 349 * @param {number} radius The dot radius. 350 * @param {Array.<string>} tooltips The points tooltips. 351 * @return {string} Returns VML markup string. 352 * @private 353 */ 354 function getVmlContent_(points, stroke, radius, tooltips) { 355 /** @type {Array.<string>} */ var dots = []; 356 for (/** @type {number} */ var i = 0; i < points.length; i++) { 357 /** @type {Array.<number>} */ var point = points[i]; 358 dots.push('<v:oval fillcolor="' + stroke + '" ' + 359 'column="' + (i + 1) + '" ' + 360 'tooltip="' + tooltips[i] + '" ' + 361 'style="' + 362 'top:' + (point[1] - radius) + 'px;' + 363 'left:' + (point[0] - radius) + 'px;' + 364 'width:' + (radius * 2) + 'px;' + 365 'height:' + (radius * 2) + 'px;' + 366 '">' + 367 '<v:stroke color="' + stroke + '" weight="' + radius + 368 '" opacity="' + options_['opacity'] + '"/>' + 369 //'<v:extrusion on="true"/>' + 370 '</v:oval>'); 371 } 372 373 return '<v:polyline strokeweight="' + options_['stroke'] + 'px" ' + 374 'strokecolor="' + stroke + '" filled="false" ' + 375 'points="' + points.join(' ') + '"/>' + dots.join(''); 376 } 377 378 /** 379 * Calculates line points. 380 * @param {Array.<number>} row The data row. 381 * @param {number} width The width. 382 * @param {number} height The height. 383 * @param {number} minValue The grid min value. 384 * @param {number} maxValue The grid max value. 385 * @return {Array.<Array>} Returns list of calculated points. 386 * @private 387 */ 388 function getPoints_(row, width, height, minValue, maxValue) { 389 /** @type {Array.<Array>} */ var points = []; 390 // TODO (alex): apply logarithmic scale by default 391 /** @type {number} * / var base = height / Math.log(maxValue);*/ 392 393 /** @type {number} */ var xPadding = options_['radius'] * 4; 394 /** @type {number} */ 395 var yPadding = options_['radius'] / 4 + (options_['grid']['lines'] - 1) / 2; 396 /** @type {boolean} */ var scale = options_['scale']; 397 /** @type {!Object} */ var params = {}; 398 if (scale) { 399 params = getScaledParams_(height, minValue, maxValue, yPadding); 400 } 401 for (/** @type {number} */ var i = 1; i < row.length; i++) { 402 /** @type {number} */ 403 var x = Math.round((i - 1) * (width - xPadding * 2) / (row.length - 2)) + 404 xPadding; 405 x = x ? x : width / 2; 406 /** @type {number} */ var y; 407 if (scale) { 408 y = scaledY_(row[i], height, yPadding, params); 409 } else { 410 y = Math.round((maxValue - parseFloat(row[i])) * 411 (height - yPadding * 2) / maxValue) + yPadding; 412 } 413 points.push([x, y]); 414 } 415 return points; 416 } 417 418 /** 419 * Calculates parameters for scaled coordinates. 420 * @param {number} height The height. 421 * @param {number} minValue The grid min value. 422 * @param {number} maxValue The grid max value. 423 * @param {number} padding The vAxis padding. 424 * @return {!Object} Parameters for scaled coordinates. 425 * @private 426 */ 427 function getScaledParams_(height, minValue, maxValue, padding) { 428 /** @type {number} */ var delta = maxValue - minValue; 429 minValue = minValue <= 0 ? 1 : minValue; 430 maxValue = maxValue <= 0 ? 1 : maxValue; 431 delta = delta <= 0 ? 1 : delta; 432 /** @type {number} */ var logDelta = Math.log(delta); 433 /** @type {number} */ 434 var minY = Math.ceil((logDelta - 435 (Math.log(maxValue) - Math.log(minValue))) * 436 (height - padding * 2) / logDelta + padding); 437 /** @type {number} */ 438 var maxY = Math.ceil((logDelta) * (height - padding * 2) / 439 logDelta + padding); 440 /** @type {number} */ var deltaY = maxY - minY; 441 return { 442 'logDelta': logDelta, 'deltaY': deltaY, 443 'minY': minY, 'maxY': maxY, 444 'minValue': minValue, 'maxValue': maxValue 445 }; 446 } 447 448 /** 449 * Gets scaled coordinates for point. 450 * @param {number} value The point value. 451 * @param {number} height The height. 452 * @param {number} padding The padding. 453 * @param {!Object} params Parameters for scaled coordinates. 454 * @return {number} vAxis coordinate. 455 * @private 456 */ 457 function scaledY_(value, height, padding, params) { 458 /** @type {number} */ var logRow = 459 Math.log(value) > 0 ? Math.log(value) : 0; 460 /** @type {number} */ 461 var y = (params['logDelta'] - (logRow - Math.log(params['minValue']))) * 462 (height - padding * 2) / params['logDelta'] + padding; 463 y = (params['deltaY'] - (y - params['minY'])) * (height - padding * 2) / 464 params['deltaY'] + padding; 465 return height - y; 466 } 467 468 /** 469 * The reference to current class instance. Used in private methods. 470 * @type {!charts.LineChart} 471 * @private 472 */ 473 var self_ = this; 474 475 /** 476 * @type {Array.<Array>} 477 * @private 478 */ 479 var data_ = null; 480 481 /** 482 * @dict 483 * @private 484 */ 485 var options_ = null; 486 487 /** 488 * @type {formatters.NumberFormatter} 489 * @private 490 */ 491 var formatter_ = null; 492 }; 493 494 // Export for closure compiler. 495 charts['LineChart'] = charts.LineChart; 496