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