1 
  2 /**
  3  * @fileoverview Simple geo 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  * GeoChart constructor.
 13  * @param {string|Element} container The HTML container.
 14  * @constructor
 15  * @extends {charts.BaseChart} charts.BaseChart
 16  * @requires charts.GeoLocations
 17  * @requires formatters.NumberFormatter
 18  * @example
 19  * <b>var</b> chart = <b>new</b> charts.GeoChart('container_id');
 20  * chart.draw([['Country', 'Population'],
 21  *             ['Germany', 200],
 22  *             ['USA',     300],
 23  *             ['Brazil',  400],
 24  *             ['Canada',  500],
 25  *             ['France',  600],
 26  *             ['Russia',  700]
 27  * ]);
 28  *
 29  * <div style="border: solid 1px #ccc; margin: 5px; padding: 5px; width: 560px">
 30  *   <div id="chart-container"
 31  *        style="width: 560px; height: 365px;"></div>
 32  * </div>
 33  * <script src="https://greylock.js.org/greylock.js"></script>
 34  * <script>
 35  *   var chart = new charts.GeoChart('chart-container');
 36  *   chart.draw([['Country', 'Population'],
 37  *               ['Germany', 200],
 38  *               ['USA',     300],
 39  *               ['Brazil',  400],
 40  *               ['Canada',  500],
 41  *               ['France',  600],
 42  *               ['Russia',  700]
 43  *   ]);
 44  * </script>
 45  */
 46 charts.GeoChart = function(container) {
 47   charts.BaseChart.apply(this, arguments);
 48 
 49   /**
 50    * Country name mapping. (e.g. usa: United States).
 51    * @enum {string}
 52    */
 53   var COUNTRY_MAPPING = {
 54     'usa': 'United States',
 55     'russia': 'Russian Federation',
 56     'drc': 'Democratic Republic of the Congo'
 57   };
 58 
 59   /**
 60    * Original svg viewport width: 950px height: 620px.
 61    * Used for scaling map to different sizes.
 62    * @enum {number}
 63    */
 64   var VIEWPORT = {
 65     width: 950,
 66     height: 620
 67   };
 68 
 69   /**
 70    * Draws the chart based on <code>data</code> and <code>opt_options</code>.
 71    * @param {!Array.<Array>} data A chart data.
 72    * @param {Object=} opt_options A optional chart's configuration options.
 73    * @override
 74    */
 75   this.draw = function(data, opt_options) {
 76     options_ = getOptions_(opt_options);
 77     self_.tooltip.setOptions(options_);
 78     /** @type {number} */ var width = self_.container.offsetWidth || 200;
 79     /** @type {number} */ var height = self_.container.offsetHeight || width;
 80 
 81     /** @type {string} */ var content = '';
 82     /** @type {number} */
 83     var scale = Math.min(width / VIEWPORT.width, height / VIEWPORT.height);
 84 
 85     /** @type {!Object.<string, Object>} */ var map = getColorMap_(data);
 86     /** @type {!Array.<Array>} */ var locations = charts.GeoLocations.DATA;
 87     for (/** @type {number} */ var i = 0; i < locations.length; i++) {
 88       /** @type {string} */ var country = getCountryKey_(locations[i][0]);
 89       /** @type {string} */ var path = locations[i][1];
 90       /** @type {number} */ var value = country in map ? map[country].value : 0;
 91       /** @type {string} */ var color = country in map ? map[country].color :
 92           /** @type {string} */ (options_['fillcolor']);
 93       /** @type {string} */
 94       var column = country in map ? map[country].column : '';
 95       /** @type {string} */
 96       var tooltip = '<b>' + getCountryName_(country) + '</b>';
 97       if (value) {
 98         tooltip += '<br>' + column + ': ';
 99         tooltip += 'number' == typeof value ?
100             formatter_.formatNumber(value) : value;
101       }
102       content += (charts.IS_SVG_SUPPORTED ? getSvgContent_ : getVmlContent_)(
103           path, tooltip, color, scale, country);
104     }
105 
106     self_.drawContent(content, width, height);
107     initEvents_();
108   };
109 
110   // Export for closure compiler.
111   this['draw'] = this.draw;
112 
113   /**
114    * @param {Array} data The data.
115    * @return {!Object.<string, Object>} Returns data colors.
116    * @private
117    */
118   function getColorMap_(data) {
119     /** @type {!Object.<string, Object>} */ var colors = {};
120     /** @type {Array.<number>} */ var range = self_.getDataRange(data, 1);
121     /** @type {!Array.<Array>} */ var rows = self_.getDataRows(data);
122     /** @type {number} */ var min = range[0];
123     /** @type {number} */ var max = range[1];
124     /** @type {string} */ var column = data[0][1];
125     for (/** @type {number} */ var i = 0; i < rows.length; i++) {
126       /** @type {string} */
127       var country = getCountryKey_(rows[i][0]).toLowerCase();
128       /** @type {number} */ var value = rows[i][1];
129       /** @type {number} */
130       var weight = (+value - min) / (max - min) * 0.8 + 0.2;
131       weight = weight ? weight : 1 * 0.8;
132       /** @type {string} */ var color = 'rgb(' +
133           getColor_(options_['rgb']['red'], weight) + ',' +
134           getColor_(options_['rgb']['green'], weight) + ',' +
135           getColor_(options_['rgb']['blue'], weight) + ')';
136       colors[country] = {color: color, value: value, column: column};
137     }
138     return colors;
139   }
140 
141   /**
142    * @param {number} component Color component: red, green or blue.
143    * @param {number} weight Color weight.
144    * @return {number} Returns color component.
145    * @private
146    */
147   function getColor_(component, weight) {
148     return parseInt(255 - (255 - component) * weight, 10);
149   }
150 
151   /**
152    * @param {string} country The country name.
153    * @return {string} Returns country key.
154    * @private
155    */
156   function getCountryKey_(country) {
157     for (var key in COUNTRY_MAPPING)
158       if (COUNTRY_MAPPING[key].toLowerCase() == country.toLowerCase())
159         return key;
160       return country;
161   }
162 
163   /**
164    * @param {string} country The country key.
165    * @return {string} Returns country name.
166    * @private
167    */
168   function getCountryName_(country) {
169     // Exceptional cases:
170     if (country in COUNTRY_MAPPING)
171       return COUNTRY_MAPPING[country];
172 
173     /** @type {Array.<string>} */
174     var words = country.replace(/\s+/g, '_').split('_');
175     for (var i = 0; i < words.length; i++) {
176       words[i] = words[i].charAt(0).toUpperCase() + words[i].substr(1);
177     }
178     return words.join(' ');
179   }
180 
181   /**
182    * @param {string} path The path.
183    * @param {string} tooltip The tooltip.
184    * @param {string} color The color.
185    * @param {number} scale The scale.
186    * @param {string} country The country key.
187    * @return {string} Returns SVG markup string.
188    * @private
189    */
190   function getSvgContent_(path, tooltip, color, scale, country) {
191     return '<path transform="scale(' + scale + ')" ' +
192            'tooltip="' + tooltip + '" d="' + path + '"' +
193            'fill="' + color + '" ' +
194            'id="' + country + '" ' +
195            'stroke="' + options_['stroke']['color'] + '"/>';
196   }
197 
198   /**
199    * @param {string} path The path.
200    * @param {string} tooltip The tooltip.
201    * @param {string} color The color.
202    * @param {number} scale The scale.
203    * @param {string} country The country key.
204    * @return {string} Returns VML markup string.
205    * @private
206    */
207   function getVmlContent_(path, tooltip, color, scale, country) {
208     //path = getVmlPath_(path);
209     path = graphics.VmlHelper.getVmlPath(path);
210 
211     /** @type {number} */ var width = self_.container.offsetWidth || 200;
212     /** @type {number} */ var height = self_.container.offsetHeight || width;
213 
214     /** @type {Array.<number>} */
215     var coordsize = [width / scale, height / scale];
216 
217     /** @type {string} */
218     var vml = '<v:shape fillcolor="' + color + '" ' +
219         'strokecolor="' + options_['stroke']['color'] + '" ' +
220         'coordorigin="0 0" ' +
221         'coordsize="' + coordsize + '" ' +
222         'style="width:100%;height:100%" ' +
223         'id="' + country + '" ' +
224         'tooltip="' + tooltip + '">' +
225         '<v:path v="' + path + '"/>' +
226         '</v:shape>';
227     return vml;
228   }
229 
230   /**
231    * Converts SVG path to VML path.
232    * @param {string} svgPath The SVG path.
233    * @return {string} Returns VML path.
234    * @deprecated Use graphics.VmlHelper.getVmlPath instead.
235    * @private
236    */
237   function getVmlPath_(svgPath) {
238     // TODO (alex): Calculate VML path.
239     // M387.56,224.4l-0.54,1.37l0.81,0.82l2.17-1.37L387.56,224.4L387.56,224.4z
240     svgPath = svgPath.replace(/(\d*)((\.*\d*)(e ?-?\d*))/g, '$1');
241     /** @type {Array.<string>} */
242     var commands = svgPath.match(/([MLHVCSQTAZ].*?)(?=[MLHVCSQTAZ]|$)/gi);
243     /** @type {string} */ var vmlPath = '';
244     /** @type {number} */ var cursorX = 0;
245     /** @type {number} */ var cursorY = 0;
246     for (/** @type {number} */ var i = 0; i < commands.length; i++) {
247       /** @type {string} */ var command = commands[i].substring(0, 1);
248       /** @type {!Array} */
249       var params = commands[i].substring(1, commands[i].length).split(/[, ]/);
250       for (/** @type {number} */ var j = 0; j < params.length; j++) {
251         //params[j] = Math.round(parseFloat(params[j]));
252       }
253       /** @type {string} */ var args = params.join();
254       /** @type {!Array} */ var coords = args.split(/[, ]+/);
255       switch (command) {
256         case 'M': // moveTo absolute
257           command = 'm';
258           cursorX = parseInt(coords[0], 10);
259           cursorY = parseInt(coords[1], 10);
260           break;
261         case 'm': // moveTo relative
262           command = 't';
263           coords[0] = parseInt(coords[0], 10) + parseInt(cursorX, 10);
264           coords[1] = parseInt(coords[1], 10) + parseInt(cursorY, 10);
265           cursorX = parseInt(coords[0], 10);
266           cursorY = parseInt(coords[1], 10);
267           //args = coords[0] + ',' + coords[1] + ' ';
268           break;
269         case 'A': // arc absolute:
270           // SVG: rx ry x-axis-rotation large-arc-flag sweep-flag x y
271           // VML: center (x,y) size(w,h) start-angle, end-angle
272           command = 'ae';
273           coords[0] = parseInt(coords[0], 10);
274           coords[1] = parseInt(coords[1], 10);
275 
276           coords[2] = parseInt(coords[2], 10);
277           coords[3] = parseInt(coords[3], 10);
278 
279           coords[4] = parseInt(coords[4], 10);
280           coords[5] = parseInt(coords[5], 10);
281           args = coords[4] + ' ' + coords[5] + ' ' + (coords[2] * 2) + ' ' +
282                  (coords[3] * 2) + ' 0 360';
283           break;
284         case 'L': // lineto absolute
285         case 'H': // horizontal lineto absolute
286           command = 'l';
287           cursorX = parseInt(coords[0], 10);
288           cursorY = parseInt(coords[1], 10);
289           break;
290         case 'l': // lineto relative
291           command = 'r';
292           //window.console && console.log(coords);
293           coords[0] = parseInt(coords[0], 10) + parseInt(cursorX, 10);
294           coords[1] = parseInt(coords[1], 10) + parseInt(cursorY, 10);
295           cursorX = parseInt(coords[0], 10);
296           cursorY = parseInt(coords[1], 10);
297           // args = coords[0] + ',' + coords[1] + ' ';
298           break;
299         case 'h': // horizontal lineto relative
300           command = 'r';
301           //window.console && console.log(coords);
302           coords[0] = parseInt(coords[0], 10) + parseInt(cursorX, 10);
303           coords[1] = cursorY;
304           cursorX = parseInt(coords[0], 10);
305           cursorY = parseInt(coords[1], 10);
306           args = coords[0] + ',' + coords[1] + ' ';
307           break;
308         case 'c':
309           command = 'v';
310           break;
311         case 'z':
312           command = 'xe';
313           args = '';
314         default:
315           command = command.toLowerCase();
316       }
317       vmlPath += command + args;
318     }
319     return vmlPath;
320   }
321 
322   /**
323    * Initializes events handlers.
324    * @private
325    */
326   function initEvents_() {
327     /** @type {Array.<Element>} */ var paths = null;
328     /** @type {!Object.<string, number>} */ var tags = {'path': 0, 'shape': 0};
329 
330     dom.events.addEventListener(
331         self_.container, dom.events.TYPE.MOUSEOUT, function(e) {
332           if (paths) {
333             for (/** @type {number} */ var i = 0; i < paths.length; i++) {
334               if (charts.IS_SVG_SUPPORTED) {
335                 //paths[i].setAttribute('stroke-width',
336                 //                  options_['stroke']['width'] + 'px');
337                 paths[i].setAttribute('stroke',
338                                   options_['stroke']['color']);
339               } else {
340                 //paths[i]['strokeweight'] = options_['stroke']['width'] + 'px';
341                 paths[i]['strokecolor'] = options_['stroke']['color'];
342               }
343             }
344             self_.tooltip.hide(e);
345             paths = null;
346           }
347         });
348 
349     dom.events.addEventListener(
350         self_.container, dom.events.TYPE.MOUSEMOVE, function(e) {
351           dom.events.dispatchEvent(self_.container, dom.events.TYPE.MOUSEOUT);
352           e = e || window.event;
353           /** @type {Element} */
354           var target = e.target || e.srcElement || e.toElement;
355           if (target && target.tagName in tags) {
356             /** @type {NodeList} */
357             var nodes = dom.getElementsByTagName(target.parentNode,
358                                                  target.tagName);
359             paths = [];
360             for (/** @type {number} */ var i = 0; i < nodes.length; i++) {
361               /** @type {Element} */ var node = nodes[i];
362               if (node.id == target.id) {
363                 paths.push(node);
364               }
365             }
366             for (i = 0; i < paths.length; i++) {
367               if (charts.IS_SVG_SUPPORTED) {
368                 //paths[i].setAttribute('stroke-width', '3px');
369                 paths[i].setAttribute('stroke', '#666');
370               } else {
371                 //paths[i]['strokeweight'] = '3px';
372                 paths[i]['strokecolor'] = '#666';
373               }
374             }
375             self_.tooltip.show(e);
376           }
377         });
378   }
379 
380   /**
381    * Gets chart's options merged with defaults chart's options.
382    * @param {Object=} opt_options A optional chart's configuration options.
383    * @return {!Object.<string, *>} A map of name/value pairs.
384    * @see charts.BaseChart#getOptions
385    * @private
386    */
387   function getOptions_(opt_options) {
388     opt_options = opt_options || {};
389     opt_options['stroke'] = opt_options['stroke'] || {};
390     opt_options['stroke']['color'] = opt_options['stroke']['color'] || '#ddd';
391     opt_options['stroke']['width'] = opt_options['stroke']['width'] || 1;
392     opt_options['fillcolor'] = opt_options['fillcolor'] || '#f5f5f5';
393     opt_options['rgb'] = opt_options['rgb'] || {};
394     opt_options['rgb']['red'] = 'red' in opt_options['rgb'] ?
395         opt_options['rgb']['red'] : 10;
396     opt_options['rgb']['green'] = 'green' in opt_options['rgb'] ?
397         opt_options['rgb']['green'] : 170;
398     opt_options['rgb']['blue'] = 'blue' in opt_options['rgb'] ?
399         opt_options['rgb']['blue'] : 10;
400     return self_.getOptions(opt_options);
401   }
402 
403   /**
404    * The reference to current class instance. Used in private methods.
405    * @type {!charts.GeoChart}
406    * @private
407    */
408   var self_ = this;
409 
410   /**
411    * @dict
412    * @private
413    */
414   var options_ = null;
415 
416   /**
417    * @type {!formatters.NumberFormatter}
418    * @private
419    */
420   var formatter_ = new formatters.NumberFormatter;
421 };
422 
423 // Export for closure compiler.
424 charts['GeoChart'] = charts.GeoChart;
425