1 2 /** 3 * @fileoverview Simple bubble 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 * BubbleChart constructor. 13 * @param {string|Element} container The HTML container. 14 * @constructor 15 * @extends {charts.BaseChart} charts.BaseChart 16 * @requires animation 17 * @requires charts.Grid 18 * @requires formatters.NumberFormatter 19 * @example 20 * <b>var</b> chart = <b>new</b> charts.BubbleChart('container_id'); 21 * chart.draw([['ID', 'X', 'Y', 'Temperature'], 22 * ['USA', 60, 60, 120], 23 * ['CAN', 25, 25, 50], 24 * ['RUS', 70, 70, 80], 25 * ['GBR', 85, 99, 40] 26 * ]); 27 * 28 * <div id="chart-container" 29 * style="width: 560px; height: 300px; margin-left: 20px;"></div> 30 * <script src="https://greylock.js.org/greylock.js"></script> 31 * <script> 32 * var chart = new charts.BubbleChart('chart-container'); 33 * chart.draw([['ID', 'X', 'Y', 'Temperature'], 34 * ['USA', 60, 60, 120], 35 * ['CAN', 25, 25, 50], 36 * ['RUS', 70, 70, 80], 37 * ['GBR', 85, 99, 40] 38 * ]); 39 * </script> 40 */ 41 charts.BubbleChart = function(container) { 42 charts.BaseChart.apply(this, arguments); 43 44 /** 45 * Draws the chart based on <code>data</code> and <code>opt_options</code>. 46 * @param {!Array.<Array>} data A chart data. 47 * @param {Object=} opt_options A optional chart's configuration options. 48 * @override 49 * @see charts.BaseChart#getOptions 50 */ 51 this.draw = function(data, opt_options) { 52 data_ = data; 53 options_ = self_.getOptions(opt_options); 54 formatter_ = new formatters.NumberFormatter( 55 /** @type {Object.<string,*>} */(options_['formatter'])); 56 self_.tooltip.setOptions(options_); 57 58 /** @type {string} */ var content = ''; 59 /** @type {!Array.<Array>} */ var rows = getDataRows_(); 60 /** @type {!Array.<string>} */ var columns = self_.getDataColumns(data); 61 /** @type {number} */ var width = self_.container.offsetWidth || 200; 62 /** @type {number} */ var height = self_.container.offsetHeight || width; 63 64 /** @type {!Array.<string>} */ var xAxisColumns = [0]; 65 /** @type {?number} */ var minY = null; 66 /** @type {number} */ var maxY = 0; 67 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 68 /** @type {Array.<number>} */ var row = rows[i]; 69 /** @type {number} */ var x = row[1]; 70 /** @type {number} */ var y = height - row[2]; 71 /** @type {number} */ var radius = row[row.length - 1]; 72 73 content += (charts.IS_CSS3_SUPPORTED ? getHtmlContent_ : 74 charts.IS_SVG_SUPPORTED ? getSvgContent_ : getVmlContent_)( 75 x, y, radius, options_['colors'][i], getTooltipText_(columns, row)); 76 77 maxY = Math.max(maxY, row[5]); 78 if (minY == null) minY = maxY; 79 minY = Math.min(minY, row[5]); 80 xAxisColumns.push(formatter_.formatNumber(row[4])); 81 } 82 83 xAxisColumns.sort(function(a, b) {return a - b}); 84 options_['data'] = {'min': minY, 'max': maxY, 'columns': xAxisColumns}; 85 options_['direction'] = charts.Grid.DIRECTION.BOTTOM_TO_TOP; 86 (new charts.Grid(self_.container)).draw(options_); 87 88 self_.drawContent(content); 89 initEvents_(); 90 }; 91 92 // Export for closure compiler. 93 this['draw'] = this.draw; 94 95 /** 96 * @param {!Array.<string>} columns The data columns. 97 * @param {Array.<number>} row The data row. 98 * @return {string} Returns tooltip content. 99 * @private 100 */ 101 function getTooltipText_(columns, row) { 102 /** @type {!Array.<string>} */ var result = []; 103 if (row[0]) result.push('<b>' + row[0] + '</b>'); 104 for (/** @type {number} */ var i = 1; i < columns.length; i++) 105 if (columns[i]) 106 result.push(columns[i] + ': <b>' + 107 formatter_.formatNumber(row[i + 3]) + '</b>'); 108 return result.join('<br>'); 109 } 110 111 /** 112 * @return {!Array.<Array>} Returns prepared data rows. 113 * @private 114 */ 115 function getDataRows_() { 116 /** @type {!Array.<Array>} */ var rows = self_.getDataRows(data_); 117 /** @type {Array.<number>} */ 118 var range = self_.getDataRange(data_, 3); 119 /** @type {number} */ var minValue = range[0]; 120 /** @type {number} */ var maxValue = range[1]; 121 /** @type {number} */ var width = self_.container.offsetWidth || 200; 122 /** @type {number} */ var height = self_.container.offsetHeight || width; 123 /** @type {number} */ 124 var limit = Math.min(width, height) * (+options_['limit'] || 16); 125 126 /** @type {Object.<string, number>} */ 127 var coords = calcBoundCoords_(rows, limit, maxValue); 128 /** @type {number} */ var maxX = coords.maxX; 129 /** @type {number} */ var maxY = coords.maxY; 130 /** @type {number} */ var minX = coords.minX; 131 /** @type {number} */ var minY = coords.minY; 132 133 // Sort data for putting smallest bubbles on the top. 134 rows.sort(sortDataRows_); 135 136 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 137 /** @type {Array.<number>} */ var row = rows[i]; 138 /** @type {number} */ 139 var radius = Math.sqrt((row[3] / maxValue * limit) / Math.PI); 140 141 row[4] = row[1]; // Saves original X coordinate for tooltip and grid. 142 row[5] = row[2]; // Saves original Y coordinate for tooltip and grid. 143 row[6] = row[3]; // Saves original value for tooltip and grid. 144 145 if (!options_['funnel']) { 146 row[1] = (row[1] - minX) / (maxX - minX) * width; 147 row[2] = (row[2] - minY) / (maxY - minY) * height; 148 } else { 149 radius = Math.sqrt((row[3]) / Math.PI) / 150 Math.sqrt((maxValue) / Math.PI) * Math.min(width, height) / 2; 151 } 152 row.push(radius); 153 } 154 return rows; 155 } 156 157 /** 158 * @param {!Array.<Array>} rows Data rows. 159 * @param {number} limit The radius limit. 160 * @param {number} maxValue The max value. 161 * @return {Object.<string, number>} Returns coords as {minX,maxX,minY,maxY}. 162 * @private 163 */ 164 function calcBoundCoords_(rows, limit, maxValue) { 165 // TODO: Merge "calcBoundCoords_" with "getBoundCoords_" to exclude 166 // duplication of similar loops. 167 /** @type {Object.<string, number>} */ var coords = getBoundCoords_(rows); 168 /** @type {number} */ var maxX = coords.maxX; 169 /** @type {number} */ var maxY = coords.maxY; 170 /** @type {number} */ var minX = coords.minX; 171 /** @type {number} */ var minY = coords.minY; 172 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 173 /** @type {Array.<number>} */ var row = rows[i]; 174 /** @type {number} */ 175 var radius = Math.sqrt((row[3] / maxValue * limit) / Math.PI); 176 minX = Math.min(minX, row[1] - radius); 177 minY = Math.min(minY, row[2] - radius); 178 maxX = Math.max(maxX, row[1] + radius); 179 maxY = Math.max(maxY, row[2] + radius); 180 } 181 return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; 182 } 183 184 /** 185 * @param {!Array.<Array>} rows Data rows. 186 * @return {Object.<string, number>} Returns coords as {minX,maxX,minY,maxY}. 187 * @private 188 */ 189 function getBoundCoords_(rows) { 190 /** @type {Object.<string, number>} */ 191 var result = {minX: 0, maxX: 0, minY: 0, maxY: 0}; 192 for (/** @type {number} */ var i = 0; i < rows.length; i++) { 193 result.maxX = Math.max(result.maxX, rows[i][1]); 194 result.maxY = Math.max(result.maxY, rows[i][2]); 195 } 196 result.minX = result.maxX; 197 result.minY = result.maxY; 198 for (i = 0; i < rows.length; i++) { 199 result.minX = Math.min(result.minX, rows[i][1]); 200 result.minY = Math.min(result.minY, rows[i][2]); 201 } 202 return result; 203 } 204 205 /** 206 * Data rows sort function. 207 * Used for putting smallest bubbles on the top. 208 * @param {Array.<number>} a Row. 209 * @param {Array.<number>} b Row. 210 * @return {number} Return rows diff. 211 * @private 212 */ 213 function sortDataRows_(a, b) { 214 // [id, x, y, value] 215 return b[3] - a[3]; 216 } 217 218 /** 219 * Initializes events handlers. 220 * @private 221 */ 222 function initEvents_() { 223 /** @type {Array|NodeList} */ var nodes; 224 if (charts.IS_CSS3_SUPPORTED) { 225 nodes = dom.getElementsByClassName(self_.container, 'circle'); 226 } else { 227 /** @type {string} */ 228 var tagName = charts.IS_SVG_SUPPORTED ? 'circle' : 'oval'; 229 nodes = dom.getElementsByTagName(self_.container, tagName); 230 } 231 232 for (/** @type {number} */ var i = 0; i < nodes.length; i++) { 233 setEvents_(nodes[i]); 234 } 235 } 236 237 /** 238 * Sets events handlers. 239 * @param {!Element} node The element. 240 * @private 241 */ 242 function setEvents_(node) { 243 /** @type {string} */ var opacity = ( 244 charts.IS_SVG_SUPPORTED && !charts.IS_CSS3_SUPPORTED ? 245 'fill-' : '') + 'opacity'; 246 /** @type {string} */ 247 var border = charts.IS_CSS3_SUPPORTED ? 'border' : 'stroke'; 248 249 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOVER, function(e) { 250 if (charts.IS_CSS3_SUPPORTED) { 251 node.style[opacity] = 1; 252 node.style[border] = 'solid 1px #ccc'; 253 } else { 254 if (window.XPathNamespace) { 255 node.setAttribute(opacity, 1); 256 node.setAttribute('stroke-width', '1'); 257 node.setAttribute('stroke', '#ccc'); 258 } else { 259 // Note: node.firstChild is <vml:fill> element. 260 (charts.IS_SVG_SUPPORTED ? node.style : node.firstChild)[opacity] = 1; 261 (charts.IS_SVG_SUPPORTED ? node.style : node)[ 262 border + (charts.IS_SVG_SUPPORTED ? '-width' : 'weight') 263 ] = '1px'; 264 (charts.IS_SVG_SUPPORTED ? node.style : node)[ 265 border + (charts.IS_SVG_SUPPORTED ? '' : 'color') 266 ] = '#ccc'; 267 if (!charts.IS_SVG_SUPPORTED) node['stroked'] = true; 268 } 269 } 270 self_.tooltip.show(e); 271 }); 272 273 dom.events.addEventListener(node, dom.events.TYPE.MOUSEOUT, function(e) { 274 if (charts.IS_CSS3_SUPPORTED) { 275 node.style[opacity] = options_['opacity']; 276 node.style.borderColor = node.style.backgroundColor; 277 } else { 278 if (window.XPathNamespace) { 279 node.setAttribute(opacity, options_['opacity']); 280 node.setAttribute('stroke-width', '0'); 281 } else { 282 // Note: node.firstChild is <vml:fill> element. 283 (charts.IS_SVG_SUPPORTED ? node.style : 284 node.firstChild)[opacity] = options_['opacity']; 285 (charts.IS_SVG_SUPPORTED ? node.style : node)[ 286 'stroke' + (charts.IS_SVG_SUPPORTED ? '-width' : 'weight') 287 ] = '0px'; 288 if (!charts.IS_SVG_SUPPORTED) node['stroked'] = false; 289 } 290 } 291 self_.tooltip.hide(e); 292 }); 293 294 dom.events.addEventListener( 295 node, dom.events.TYPE.MOUSEMOVE, self_.tooltip.show); 296 297 dom.events.dispatchEvent(node, dom.events.TYPE.MOUSEOUT); 298 initAnimation_(node); 299 } 300 301 /** 302 * Initializes bubble animation. 303 * @param {!Element|Node} node The element. 304 * @private 305 */ 306 function initAnimation_(node) { 307 // Animation for SVG nodes is implemented by <svg:animate/> tag. 308 // Perform animation for VML and HTML nodes. 309 if (!charts.IS_SVG_SUPPORTED || charts.IS_CSS3_SUPPORTED) { 310 var size = parseFloat(node.style.width) || node.offsetWidth; 311 var x = parseFloat(node.style.left) || node.offsetLeft; 312 var y = parseFloat(node.style.top) || node.offsetTop; 313 node.style.width = '1px'; 314 node.style.height = '1px'; 315 node.style.left = (x + size / 2) + 'px'; 316 node.style.top = (y + size / 2) + 'px'; 317 animation.animate(node, { 318 'width': size, 'height': size, 'left': x, 319 'top': y, 'transform': 'scale(1.0)' 320 }); 321 } 322 } 323 324 /** 325 * @param {number} x The X coord. 326 * @param {number} y The Y coord. 327 * @param {number} radius The bubble radius. 328 * @param {string} color The bubble color. 329 * @param {string} tooltip The bubble tooltip. 330 * @return {string} Returns HTML markup string. 331 * @private 332 */ 333 function getHtmlContent_(x, y, radius, color, tooltip) { 334 return '<div class="circle" style="position:absolute;border-radius:50%;' + 335 'opacity:' + options_['opacity'] + ';' + 336 'width:' + (radius * 2) + 'px;' + 337 'height:' + (radius * 2) + 'px;' + 338 'top:' + (y - radius) + 'px;' + 339 'left:' + (x - radius) + 'px;' + 340 'background:' + color + ';' + 341 'border: solid 1px ' + color + ';' + 342 '-moz-transform:scale(0);' + 343 '-webkit-transform:scale(0);' + 344 '-o-transform:scale(0);' + 345 '" title="' + tooltip + '"></div>'; 346 } 347 348 /** 349 * @param {number} x The X coord. 350 * @param {number} y The Y coord. 351 * @param {number} radius The bubble radius. 352 * @param {string} color The bubble color. 353 * @param {string} tooltip The bubble tooltip. 354 * @return {string} Returns SVG markup string. 355 * @private 356 */ 357 function getSvgContent_(x, y, radius, color, tooltip) { 358 /** @type {string} */ 359 var duration = (Math.random() * (0.6 - 0.2) + 0.2).toFixed(2); 360 /** @type {string} */ 361 var delay = (Math.random() * (0.3 - 0.1) + 0.1).toFixed(2); 362 363 return '<circle cx="' + x + '" cy="' + y + '" r="' + 364 radius + '" fill="' + color + '" tooltip="' + tooltip + '" ' + 365 // 'stroke="' + color + '" stroke-width="0" ' + 366 'fill-opacity="' + options_['opacity'] + '">' + 367 368 '<animate attributeName="r" dur="' + duration + 's" from="0" to="' + 369 (radius * 1.2) + '"/>' + 370 '<animate attributeName="r" dur="' + delay + 's" from="' + 371 (radius * 1.2) + '" to="' + radius + '" begin="' + duration + 's"/>' + 372 373 '<animate attributeName="fill-opacity" from="0" to="' + 374 options_['opacity'] + '" dur="' + 375 (parseFloat(duration) + parseFloat(delay)) + 's"/>' + 376 '</circle>'; 377 } 378 379 /** 380 * @param {number} x The X coord. 381 * @param {number} y The Y coord. 382 * @param {number} radius The bubble radius. 383 * @param {string} color The bubble color. 384 * @param {string} tooltip The bubble tooltip. 385 * @return {string} Returns VML markup string. 386 * @private 387 */ 388 function getVmlContent_(x, y, radius, color, tooltip) { 389 return '<v:oval ' + 390 'style="' + 391 'top:' + (y - radius) + 'px;' + 392 'left:' + (x - radius) + 'px;' + 393 'width:' + (radius * 2) + 'px;' + 394 'height:' + (radius * 2) + 'px;' + 395 '" tooltip="' + tooltip + '"' + 396 // ' strokecolor="' + color + '" strokeweight="0"' + 397 // ' stroked="true"' + 398 '>' + 399 '<v:fill color="' + color + '"' + 400 '" opacity="' + options_['opacity'] + '"/>' + 401 '</v:oval>'; 402 } 403 404 /** 405 * The reference to current class instance. Used in private methods. 406 * @type {!charts.BubbleChart} 407 * @private 408 */ 409 var self_ = this; 410 411 /** 412 * @type {Array.<Array>} 413 * @private 414 */ 415 var data_ = null; 416 417 /** 418 * @dict 419 * @private 420 */ 421 var options_ = null; 422 423 /** 424 * @type {!formatters.NumberFormatter} 425 * @private 426 */ 427 var formatter_ = new formatters.NumberFormatter; 428 }; 429 430 // Export for closure compiler. 431 charts['BubbleChart'] = charts.BubbleChart; 432