1 
  2 /**
  3  * @fileoverview Simple pie 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  * PieChart constructor.
 13  * @param {string|Element} container The HTML container.
 14  * @constructor
 15  * @extends {charts.BaseChart} charts.BaseChart
 16  * @requires formatters.NumberFormatter
 17  * @example
 18  * <b>var</b> chart = <b>new</b> charts.PieChart('container_id');
 19  * chart.draw([['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep'],
 20  *             [100, 50, 30, 10, 40], [140, 2, 110, 150, 1300]]);
 21  *
 22  * <div style="border: solid 1px #ccc; margin: 5px; padding: 5px; width: 560px">
 23  *   <div id="chart-container"
 24  *        style="width: 560px; height: 300px;"></div>
 25  * </div>
 26  * <script src="https://greylock.js.org/greylock.js"></script>
 27  * <script>
 28  *   var chart = new charts.PieChart('chart-container');
 29  *   chart.draw([['Work', 'Eat', 'Commute', 'Watch TV', 'Sleep'],
 30  *               [100, 50, 30, 10, 40], [140, 2, 110, 150, 1300]]);
 31  * </script>
 32  */
 33 charts.PieChart = function(container) {
 34   charts.BaseChart.apply(this, arguments);
 35 
 36   /**
 37    * Draws the chart based on <code>data</code> and <code>opt_options</code>.
 38    * @param {!Array.<Array>} data A chart data.
 39    * @param {Object=} opt_options A optional chart's configuration options.
 40    * @see charts.BaseChart#getOptions
 41    * @override
 42    * @example
 43    * options: {
 44    *   'animation': {'radius': 0.7}
 45    *   'font': {'color': '#fff'}
 46    *   'stroke': {'stroke': 1}
 47    * }
 48    */
 49   this.draw = function(data, opt_options) {
 50     data_ = data;
 51     options_ = getOptions_(opt_options);
 52     formatter_ = new formatters.NumberFormatter(
 53         /** @type {Object.<string,*>} */(options_['formatter']));
 54     tooltip_.setOptions(options_);
 55 
 56     /** @type {!Array.<number>} */ var rows = [];
 57     for (/** @type {number} */ var i = 1; i < data.length; i++) {
 58       for (/** @type {number} */ var j = 0; j < data[i].length; j++) {
 59         rows[j] = (rows[j] || 0) + data[i][j];
 60       }
 61     }
 62 
 63     /** @type {!Array.<number>} */ var angles = [];
 64     /** @type {!Array.<string>} */ var tooltips = [];
 65     /** @type {number} */ var len = rows.length;
 66     /** @type {number} */ var total = 0.0001;
 67     for (i = 0; i < len;) total += rows[i++];
 68 
 69     for (i = 0; i < len; i++) {
 70       /** @type {number} */ var value = rows[i];
 71       if (void 0 != value) {
 72         /** @type {number} */ var ratio = value / total > 0.999 ?
 73                                           0.999 : value / total;
 74         /** @type {number} */ var angle = 360 * ratio;
 75         /** @type {number} */ var percent = len > 1 ? angle / 360 * 100 : 100;
 76         angles.push(angle);
 77         tooltips.push(formatter_.formatNumber(value) +
 78             ' (' + percent.toFixed(1) + '%)');
 79       }
 80     }
 81 
 82     draw_(angles, tooltips, total);
 83     setTimeout(initEvents_, 100);
 84   };
 85 
 86   // Export for closure compiler.
 87   this['draw'] = this.draw;
 88 
 89   /**
 90    * Gets chart's options merged with defaults chart's options.
 91    * @param {Object=} opt_options A optional chart's configuration options.
 92    * @return {!Object.<string, *>} A map of name/value pairs.
 93    * @see charts.BaseChart#getOptions
 94    * @private
 95    * @example
 96    * options: {
 97    *   'animation': {'radius': 0.7}
 98    *   'font': {'color': '#fff'}
 99    *   'stroke': {'stroke': 1}
100    * }
101    */
102   function getOptions_(opt_options) {
103     opt_options = opt_options || {};
104     opt_options['font'] = opt_options['font'] || {};
105     opt_options['font']['color'] = opt_options['font']['color'] || '#fff';
106     opt_options['stroke'] = opt_options['stroke'] || {};
107     opt_options['stroke']['size'] = opt_options['stroke']['size'] || 1;
108     opt_options['animation'] = opt_options['animation'] || {};
109     opt_options['formatter'] = opt_options['formatter'] || {};
110     opt_options['animation']['radius'] =
111         opt_options['animation']['radius'] || 0.7;
112     return self_.getOptions(opt_options);
113   }
114 
115   /**
116    * Initializes events handlers.
117    * @private
118    */
119   function initEvents_() {
120     /** @type {string} */
121     var tagName = charts.IS_SVG_SUPPORTED ? 'path' : 'shape';
122     /** @type {NodeList} */
123     var nodes = dom.getElementsByTagName(self_.container, tagName);
124     for (/** @type {number} */ var i = 0; i < nodes.length; i++) {
125       setEvents_(nodes[i]);
126     }
127   }
128 
129   /**
130    * Sets events handlers.
131    * @param {!Element} node The element.
132    * @private
133    */
134   function setEvents_(node) {
135     /** @type {string} */ var attr = 'opacity';
136     /** @type {!Object.<string, function(Event,...)>} */ var events = {};
137 
138     events[dom.events.TYPE.MOUSEMOVE] = function(e) {
139       tooltip_.show(e);
140     };
141 
142     events[dom.events.TYPE.MOUSEOVER] = function(e) {
143       // Note: node.firstChild is <vml:fill> element.
144       (charts.IS_SVG_SUPPORTED ? node.style : node.firstChild)[attr] = 1;
145       tooltip_.show(e);
146     };
147 
148     events[dom.events.TYPE.MOUSEOUT] = function(e) {
149       // Note: node.firstChild is <vml:fill> element.
150       (charts.IS_SVG_SUPPORTED ? node.style : node.firstChild)[attr] =
151           options_['opacity'];
152       tooltip_.hide(e);
153     };
154 
155     for (/** @type {string} */ var key in events) {
156       dom.events.addEventListener(node, key, events[key]);
157 
158       // Add the same listener to SVG text element.
159       if (charts.IS_SVG_SUPPORTED)
160         dom.events.addEventListener(node.nextSibling, key, events[key]);
161     }
162 
163     events[dom.events.TYPE.MOUSEOUT](null);
164     // dom.events.dispatchEvent(node, dom.events.TYPE.MOUSEOUT);
165   }
166 
167   /**
168    * Calculates pie piece coordinates.
169    * @param {number} width The chart width.
170    * @param {number} height The chart height.
171    * @param {number} radius The piece radius.
172    * @param {number} startAngle The piece start angle.
173    * @param {number} endAngle The piece end angle.
174    * @return {Object.<string, number>} Returns pie piece coordinates.
175    * @private
176    */
177   function getCoords_(width, height, radius, startAngle, endAngle) {
178     /** @type {number} */ var fix = charts.IS_SVG_SUPPORTED ? 0 : 360;
179     /** @type {number} */
180     var offset = Math.PI * (startAngle + (endAngle + fix - startAngle) / 2);
181     /** @type {number} */ var halfWidth = width / 2;
182     /** @type {number} */ var halfHeight = height / 2;
183     return {
184       x1: halfWidth + radius * Math.cos(Math.PI * startAngle / 180),
185       y1: halfHeight + radius * Math.sin(Math.PI * startAngle / 180),
186       x2: halfWidth + radius * Math.cos(Math.PI * endAngle / 180),
187       y2: halfHeight + radius * Math.sin(Math.PI * endAngle / 180),
188       tx: halfWidth + (radius * 0.75) * Math.cos(offset / 180),
189       ty: halfHeight + (radius * 0.75) * Math.sin(offset / 180)
190     };
191   }
192 
193   /**
194    * @param {!Array.<number>} angles The list of piece's angles.
195    * @param {!Array.<string>} tooltips The list of piece's tooltips.
196    * @param {number} total The total value.
197    * @private
198    */
199   function draw_(angles, tooltips, total) {
200     /** @type {number} */ var startAngle = 0;
201     /** @type {number} */ var endAngle = charts.IS_SVG_SUPPORTED ? 270 : 90;
202     /** @type {number} */ var width = self_.container.offsetWidth || 200;
203     /** @type {number} */ var height = self_.container.offsetHeight || width;
204     /** @type {number} */ var radius = Math.min(width, height) / 2;
205     /** @type {number} */ var length = angles.length;
206     /** @type {string} */ var content = '';
207 
208     /** @type {!Array.<string>} */ var colors = [].concat(options_['colors']);
209     for (/** @type {number} */ var i = 0; i < length; i++) {
210       /** @type {string} */ var color = colors.shift();
211       colors.push(color);
212       /** @type {number} */ var angle = angles[i];
213       /** @type {number} */ var percent = length > 1 ? angle / 360 * 100 : 100;
214       /** @type {string} */ var percents = percent.toFixed(1);
215       /** @type {string} */ var column = self_.getDataColumns(data_)[i];
216       try {
217         column = decodeURI(column);
218       } catch (ex) {}
219       column = unescape(column).replace(/\"/g, ' ');
220 
221       /** @type {string} */
222       var tooltip = column ? (column + '<br><b>' + tooltips[i] + '</b>') : '';
223 
224       startAngle = endAngle;
225       endAngle = startAngle + angle;
226       /** @type {Object.<string, number>} */
227       var coords = getCoords_(width, height, radius, startAngle, endAngle);
228       /** @type {number} */
229       var textOffsetX = (12 + (('%' + percents).length - 3) * 4);
230       /** @type {number} */
231       var textOffsetY = parseInt(options_['font']['size'] / 2, 10);
232 
233       if (charts.IS_SVG_SUPPORTED) {
234         content += getSvgContent_(
235             width, height, coords.x1, coords.y1, coords.x2, coords.y2, radius,
236             coords.tx - textOffsetX, coords.ty + textOffsetY,
237             color, percents, tooltip, startAngle, endAngle);
238       } else {
239         content += getVmlContent_(
240             width, height, radius,
241             coords.tx - textOffsetX, coords.ty + textOffsetY,
242             color, percents, tooltip, startAngle, endAngle);
243       }
244     }
245 
246     self_.drawContent(content, width, height);
247   }
248 
249   /**
250    * @param {number} width The width.
251    * @param {number} height The height.
252    * @param {number} x1 The X1.
253    * @param {number} y1 The Y1.
254    * @param {number} x2 The X2.
255    * @param {number} y2 The Y2.
256    * @param {number} radius The radius.
257    * @param {number} tx The text X.
258    * @param {number} ty The text Y.
259    * @param {string} fill The fill color.
260    * @param {string} percents The text.
261    * @param {string} tooltip The tooltip.
262    * @param {number} startAngle The startAngle.
263    * @param {number} endAngle The endAngle.
264    * @return {string} Returns SVG markup string.
265    * @private
266    */
267   function getSvgContent_(
268       width, height, x1, y1, x2, y2, radius, tx, ty, fill, percents, tooltip,
269       startAngle, endAngle) {
270     // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
271     /** @type {number} */ var flag = parseInt(percents, 10) > 50 ? 1 : 0;
272     /** @type {string} */
273     var path = 'M' + (width / 2) + ',' + (height / 2) +
274                'L' + x1 + ',' + y1 +
275                'A' + radius + ',' + radius + ' 0,' +
276                 flag + ',1,' + x2 + ',' + y2 +
277                'L' + (width / 2) + ',' + (height / 2) +
278                'A0,0,0,0,0,' + (width / 2) + ',' + (height / 2);
279 
280     return '<g>' +
281         '<path d="' + path + '" fill="' + fill + '" ' + 'stroke="' +
282         options_['font']['color'] + '" ' +
283         'stroke-width="' + options_['stroke']['size'] + '" ' +
284         'tooltip="' + tooltip + '"' +
285         '>' +
286         '<animate attributeName="d" dur="0.5s" values="' +
287         getSvgAnimatedPath_(width, height, radius, startAngle, endAngle) +
288         '" calcMode="discrete"/>' +
289         '<animate attributeName="fill-opacity" from="0" to="' +
290         options_['opacity'] + '" dur="0.5s"/>' +
291         '</path>' +
292 
293         '<text text-anchor="start" ' +
294         'font-family="' + options_['font']['family'] + '" ' +
295         'font-size="' + options_['font']['size'] + '" ' +
296         'x="' + tx + '" ' +
297         'y="' + ty + '" ' + ' style="cursor:default;' +
298         'text-shadow:0.1px 0.1px 1px #000" ' +
299         'stroke="none" stroke-width="0" fill="' +
300         options_['font']['color'] + '">' +
301         (percents < 5 ? '' : percents + '%') + '</text>' +
302         '</g>';
303   }
304   /**
305    * @param {number} width The width.
306    * @param {number} height The height.
307    * @param {number} radius The radius.
308    * @param {number} startAngle The startAngle.
309    * @param {number} endAngle The endAngle.
310    * @return {string} Returns SVG animation path string.
311    * @private
312    */
313   function getSvgAnimatedPath_(width, height, radius, startAngle, endAngle) {
314     /** @type {!Array.<string>} */ var paths = [];
315     /** @type {number} */ var steps = 20;
316     /** @type {number} */ var randomStart = Math.random() * 360 + 270;
317     /** @type {number} */ var randomEnd = Math.random() * 360 + 270;
318     /** @type {number} */ var r = radius * options_['animation']['radius'];
319     /** @type {number} */ var switcher = 0;
320     if (randomStart > randomEnd) {
321       switcher = randomStart;
322       randomStart = randomEnd;
323       randomEnd = switcher;
324     }
325     for (/** @type {number} */ var i = steps; i > 0; i--) {
326       /** @type {number} */
327       var angle1 = startAngle + (randomStart - startAngle) / steps * i;
328       /** @type {number} */
329       var angle2 = endAngle + (randomEnd - endAngle) / steps * i;
330       var radius1 = radius + (r - radius) / steps * i;
331       /** @type {Object.<string, number>} */
332       var coords = getCoords_(width, height, radius1, angle1, angle2);
333       // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
334       /** @type {number} */ var flag = (angle2 - angle1) > 180 ? 1 : 0;
335       paths.push('M' + (width / 2) + ',' + (height / 2) +
336           'L' + parseInt(coords.x1, 10) + ',' + parseInt(coords.y1, 10) +
337           'A' + radius1 + ',' + radius1 + ' 0,' +
338           flag + ',1,' + parseInt(coords.x2, 10) + ',' +
339           parseInt(coords.y2, 10) +
340           'L' + (width / 2) + ',' + (height / 2) +
341           'A0,0,0,0,0,' + (width / 2) + ',' + (height / 2));
342     }
343     return paths.join('; ');
344   }
345 
346   /**
347    * @param {number} width The width.
348    * @param {number} height The height.
349    * @param {number} radius The radius.
350    * @param {number} tx The text X.
351    * @param {number} ty The text Y.
352    * @param {string} fill The fill color.
353    * @param {string} percents The percents text.
354    * @param {number} startAngle The startAngle.
355    * @param {number} endAngle The endAngle.
356    * @param {string} tooltip The tooltip name.
357    * @return {string} Returns VML markup string.
358    * @private
359    * @see http://www.w3.org/TR/NOTE-VML#_Toc416858391
360    */
361   function getVmlContent_(
362       width, height, radius, tx, ty, fill, percents, tooltip,
363       startAngle, endAngle) {
364     /** @type {number} */ var fixedWidth = parseInt(width / 2, 10);
365     /** @type {number} */ var fixedHeight = parseInt(height / 2, 10);
366     /** @type {string} */
367     var path = 'M ' + fixedWidth + ' ' + fixedHeight + ' ' +
368                'AE ' + fixedWidth + ' ' + fixedHeight + ' ' +
369                parseInt(radius, 10) + ' ' + parseInt(radius, 10) + ' ' +
370                Math.round(startAngle * 65535) + ' ' +
371                (-Math.round(-(endAngle - startAngle) * 65536)) +
372                ' X E';
373     return '<v:shape path="' + path + '" ' +
374         'strokeweight="' + options_['stroke']['size'] + 'px" ' +
375         'strokecolor="' + options_['font']['color'] + '" ' +
376         'fillcolor="' + fill + '" ' +
377         'tooltip="' + tooltip + '"' +
378         'style="width:' + width + 'px;height:' + height + 'px;flip:x">' +
379         '<v:fill opacity="1" color="' + fill + '"/>' +
380 
381         '<v:textbox style="color:' + options_['font']['color'] + ';" ' +
382         'tooltip="' + tooltip + '"' +
383         '>' +
384         '<div style="position:relative;cursor:default;width:50px;' +
385         'font-family:' + options_['font']['family'] + '; ' +
386         'font-size:' + options_['font']['size'] + 'px; ' +
387         'top:' + (ty - 18) + 'px;' +
388         'left:' + (tx - 10) + 'px;">' +
389         (percents < 5 ? '' : percents + '%') + '</div></v:textbox>' +
390         // '<v:extrusion on="true"/>' +
391         '</v:shape>';
392   }
393 
394   /**
395    * The reference to current class instance. Used in private methods.
396    * @type {!charts.PieChart}
397    * @private
398    */
399   var self_ = this;
400 
401   /**
402    * @type {Array.<Array>}
403    * @private
404    */
405   var data_ = null;
406 
407   /**
408    * @dict
409    * @private
410    */
411   var options_ = null;
412 
413   /**
414    * @type {formatters.NumberFormatter}
415    * @private
416    */
417   var formatter_ = null;
418 
419   /**
420    * Instance of <code>charts.Tooltip</code>.
421    * @type {!charts.Tooltip}
422    * @see charts.Tooltip
423    * @private
424    */
425   var tooltip_ = self_.tooltip;
426 };
427 
428 // Export for closure compiler.
429 charts['PieChart'] = charts.PieChart;
430