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