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