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