1/* 2Plugin Name: amCharts Data Loader 3Description: This plugin adds external data loading capabilities to all amCharts libraries. 4Author: Martynas Majeris, amCharts 5Version: 1.0.15 6Author URI: http://www.amcharts.com/ 7 8Copyright 2015 amCharts 9 10Licensed under the Apache License, Version 2.0 (the "License"); 11you may not use this file except in compliance with the License. 12You may obtain a copy of the License at 13 14 http://www.apache.org/licenses/LICENSE-2.0 15 16Unless required by applicable law or agreed to in writing, software 17distributed under the License is distributed on an "AS IS" BASIS, 18WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19See the License for the specific language governing permissions and 20limitations under the License. 21 22Please note that the above license covers only this plugin. It by all means does 23not apply to any other amCharts products that are covered by different licenses. 24*/ 25 26/** 27 * TODO: 28 * incremental load 29 * XML support (?) 30 */ 31 32/* globals AmCharts, ActiveXObject */ 33/* jshint -W061 */ 34 35/** 36 * Initialize language prompt container 37 */ 38AmCharts.translations.dataLoader = {}; 39 40/** 41 * Set init handler 42 */ 43AmCharts.addInitHandler( function( chart ) { 44 45 /** 46 * Check if dataLoader is set (initialize it) 47 */ 48 if ( undefined === chart.dataLoader || !isObject( chart.dataLoader ) ) 49 chart.dataLoader = {}; 50 51 /** 52 * Check charts version for compatibility: 53 * the first compatible version is 3.13 54 */ 55 var version = chart.version.split( '.' ); 56 if ( ( Number( version[ 0 ] ) < 3 ) || ( 3 === Number( version[ 0 ] ) && ( Number( version[ 1 ] ) < 13 ) ) ) 57 return; 58 59 /** 60 * Define object reference for easy access 61 */ 62 var l = chart.dataLoader; 63 l.remaining = 0; 64 l.percentLoaded = {}; 65 66 /** 67 * Set defaults 68 */ 69 var defaults = { 70 'async': true, 71 'format': 'json', 72 'showErrors': true, 73 'showCurtain': true, 74 'noStyles': false, 75 'reload': 0, 76 'timestamp': false, 77 'delimiter': ',', 78 'skip': 0, 79 'skipEmpty': true, 80 'emptyAs': undefined, 81 'useColumnNames': false, 82 'init': false, 83 'progress': false, 84 'reverse': false, 85 'reloading': false, 86 'complete': false, 87 'error': false, 88 'headers': [], 89 'chart': chart 90 }; 91 92 /** 93 * Create a function that can be used to load data (or reload via API) 94 */ 95 l.loadData = function() { 96 97 /** 98 * Load all files in a row 99 */ 100 if ( 'stock' === chart.type ) { 101 102 // delay this a little bit so the chart has the chance to build itself 103 setTimeout( function() { 104 105 // preserve animation 106 if ( 0 > chart.panelsSettings.startDuration ) { 107 l.startDuration = chart.panelsSettings.startDuration; 108 chart.panelsSettings.startDuration = 0; 109 } 110 111 // cycle through all of the data sets 112 for ( var x = 0; x < chart.dataSets.length; x++ ) { 113 var ds = chart.dataSets[ x ]; 114 115 // load data 116 if ( undefined !== ds.dataLoader && undefined !== ds.dataLoader.url ) { 117 118 callFunction( ds.dataLoader.init, ds.dataLoader, chart ); 119 ds.dataProvider = []; 120 applyDefaults( ds.dataLoader ); 121 loadFile( ds.dataLoader.url, ds, ds.dataLoader, 'dataProvider' ); 122 123 } 124 125 // load events data 126 if ( undefined !== ds.eventDataLoader && undefined !== ds.eventDataLoader.url ) { 127 128 callFunction( ds.eventDataLoader.init, ds.eventDataLoader, chart ); 129 ds.events = []; 130 applyDefaults( ds.eventDataLoader ); 131 loadFile( ds.eventDataLoader.url, ds, ds.eventDataLoader, 'stockEvents' ); 132 133 } 134 } 135 136 }, 100 ); 137 138 } else { 139 140 callFunction( l.init, l, chart ); 141 142 applyDefaults( l ); 143 144 if ( undefined === l.url ) 145 return; 146 147 // preserve animation 148 if ( undefined !== chart.startDuration && ( 0 < chart.startDuration ) ) { 149 l.startDuration = chart.startDuration; 150 chart.startDuration = 0; 151 } 152 153 if ( 'gauge' === chart.type ) { 154 // set empty data set 155 if ( undefined === chart.arrows ) 156 chart.arrows = []; 157 158 loadFile( l.url, chart, l, 'arrows' ); 159 } else { 160 // set empty data set 161 if ( undefined === chart.dataProvider ) 162 chart.dataProvider = chart.type === 'map' ? {} : []; 163 164 loadFile( l.url, chart, l, 'dataProvider' ); 165 } 166 167 } 168 169 }; 170 171 /** 172 * Trigger load 173 */ 174 l.loadData(); 175 176 /** 177 * Loads a file and determines correct parsing mechanism for it 178 */ 179 function loadFile( url, holder, options, providerKey ) { 180 181 // set default providerKey 182 if ( undefined === providerKey ) 183 providerKey = 'dataProvider'; 184 185 // show curtain 186 if ( options.showCurtain ) 187 showCurtain( undefined, options.noStyles ); 188 189 // increment loader count 190 l.remaining++; 191 192 // set percent loaded for this file 193 l.percentLoaded[ url ] = 0; 194 195 // hijack user-defined "progress" handler with our own, so that we can 196 // track progress 197 if ( options.progress !== undefined && typeof( options.progress ) === 'function' && options._progress === undefined ) { 198 options._progress = options.progress; 199 options.progress = function( percent ) { 200 // set progress 201 l.percentLoaded[ url ] = percent; 202 203 // calculate global percent 204 var totalPercent = 0; 205 var fileCount = 0; 206 for ( var x in l.percentLoaded ) { 207 if ( l.percentLoaded.hasOwnProperty( x ) ) { 208 fileCount++; 209 totalPercent += l.percentLoaded[ x ]; 210 } 211 } 212 var globalPercent = Math.round( ( totalPercent / fileCount ) * 100 ) / 100; 213 214 // call user function 215 options._progress.call( this, globalPercent, Math.round( percent * 100 ) / 100, url ); 216 }; 217 } 218 219 // load the file 220 AmCharts.loadFile( url, options, function( response ) { 221 222 // error? 223 if ( false === response ) { 224 callFunction( options.error, options, chart ); 225 raiseError( AmCharts.__( 'Error loading the file', chart.language ) + ': ' + url, false, options ); 226 } else { 227 228 // determine the format 229 if ( undefined === options.format ) { 230 // TODO 231 options.format = 'json'; 232 } 233 234 // lowercase 235 options.format = options.format.toLowerCase(); 236 237 // invoke parsing function 238 switch ( options.format ) { 239 240 case 'json': 241 242 holder[ providerKey ] = AmCharts.parseJSON( response ); 243 244 if ( false === holder[ providerKey ] ) { 245 callFunction( options.error, options, chart ); 246 raiseError( AmCharts.__( 'Error parsing JSON file', chart.language ) + ': ' + l.url, false, options ); 247 holder[ providerKey ] = []; 248 return; 249 } else { 250 holder[ providerKey ] = postprocess( holder[ providerKey ], options ); 251 callFunction( options.load, options, chart ); 252 } 253 254 break; 255 256 case 'csv': 257 258 holder[ providerKey ] = AmCharts.parseCSV( response, options ); 259 260 if ( false === holder[ providerKey ] ) { 261 callFunction( options.error, options, chart ); 262 raiseError( AmCharts.__( 'Error parsing CSV file', chart.language ) + ': ' + l.url, false, options ); 263 holder[ providerKey ] = []; 264 return; 265 } else { 266 holder[ providerKey ] = postprocess( holder[ providerKey ], options ); 267 callFunction( options.load, options, chart ); 268 } 269 270 break; 271 272 default: 273 callFunction( options.error, options, chart ); 274 raiseError( AmCharts.__( 'Unsupported data format', chart.language ) + ': ' + options.format, false, options.noStyles ); 275 return; 276 } 277 278 // decrement remaining counter 279 l.remaining--; 280 281 // we done? 282 if ( 0 === l.remaining ) { 283 284 // callback 285 callFunction( options.complete, chart ); 286 287 // take in the new data 288 if ( options.async ) { 289 290 if ( 'map' === chart.type ) { 291 292 // take in new data 293 chart.validateNow( true ); 294 295 // remove curtain 296 removeCurtain(); 297 298 } else { 299 300 // add a dataUpdated event to handle post-load stuff 301 if ( 'gauge' !== chart.type ) { 302 chart.addListener( 'dataUpdated', function( event ) { 303 304 // restore default period (stock chart) 305 if ( 'stock' === chart.type && !options.reloading && undefined !== chart.periodSelector ) { 306 chart.periodSelector.setDefaultPeriod(); 307 } 308 309 // remove curtain 310 removeCurtain(); 311 312 // remove this listener 313 chart.events.dataUpdated.pop(); 314 } ); 315 } 316 317 318 // take in new data 319 chart.validateData(); 320 321 // invalidate size for the pie chart 322 // disabled for now as it is not longer necessary 323 /*if ( 'pie' === chart.type && chart.invalidateSize !== undefined ) 324 chart.invalidateSize();*/ 325 326 // gauge chart does not trigger dataUpdated event 327 // let's explicitly remove the curtain for it 328 if ( 'gauge' === chart.type ) 329 removeCurtain(); 330 331 // make the chart animate again 332 if ( l.startDuration ) { 333 if ( 'stock' === chart.type ) { 334 chart.panelsSettings.startDuration = l.startDuration; 335 for ( var x = 0; x < chart.panels.length; x++ ) { 336 chart.panels[ x ].startDuration = l.startDuration; 337 chart.panels[ x ].animateAgain(); 338 } 339 } else { 340 chart.startDuration = l.startDuration; 341 if ( chart.animateAgain !== undefined ) 342 chart.animateAgain(); 343 } 344 } 345 } 346 } 347 348 } 349 350 // schedule another load if necessary 351 if ( options.reload ) { 352 353 if ( options.timeout ) 354 clearTimeout( options.timeout ); 355 356 options.timeout = setTimeout( loadFile, 1000 * options.reload, url, holder, options ); 357 options.reloading = true; 358 359 } 360 361 } 362 363 } ); 364 365 } 366 367 /** 368 * Checks if postProcess is set and invokes the handler 369 */ 370 function postprocess( data, options ) { 371 if ( undefined !== options.postProcess && isFunction( options.postProcess ) ) 372 try { 373 return options.postProcess.call( l, data, options, chart ); 374 } catch ( e ) { 375 raiseError( AmCharts.__( 'Error loading file', chart.language ) + ': ' + options.url, false, options ); 376 return data; 377 } else 378 return data; 379 } 380 381 /** 382 * Returns true if argument is array 383 */ 384 function isObject( obj ) { 385 return 'object' === typeof( obj ); 386 } 387 388 /** 389 * Returns true is argument is a function 390 */ 391 function isFunction( obj ) { 392 return 'function' === typeof( obj ); 393 } 394 395 /** 396 * Applies defaults to config object 397 */ 398 function applyDefaults( obj ) { 399 for ( var x in defaults ) { 400 if ( defaults.hasOwnProperty( x ) ) 401 setDefault( obj, x, defaults[ x ] ); 402 } 403 } 404 405 /** 406 * Checks if object property is set, sets with a default if it isn't 407 */ 408 function setDefault( obj, key, value ) { 409 if ( undefined === obj[ key ] ) 410 obj[ key ] = value; 411 } 412 413 /** 414 * Raises an internal error (writes it out to console) 415 */ 416 function raiseError( msg, error, options ) { 417 418 if ( options.showErrors ) 419 showCurtain( msg, options.noStyles ); 420 else { 421 removeCurtain(); 422 console.log( msg ); 423 } 424 425 } 426 427 /** 428 * Shows curtain over chart area 429 */ 430 function showCurtain( msg, noStyles ) { 431 432 // remove previous curtain if there is one 433 removeCurtain(); 434 435 // did we pass in the message? 436 if ( undefined === msg ) 437 msg = AmCharts.__( 'Loading data...', chart.language ); 438 439 // create and populate curtain element 440 var curtain = document.createElement( 'div' ); 441 curtain.setAttribute( 'id', chart.div.id + '-curtain' ); 442 curtain.className = 'amcharts-dataloader-curtain'; 443 444 if ( true !== noStyles ) { 445 curtain.style.position = 'absolute'; 446 curtain.style.top = 0; 447 curtain.style.left = 0; 448 curtain.style.width = ( undefined !== chart.realWidth ? chart.realWidth : chart.divRealWidth ) + 'px'; 449 curtain.style.height = ( undefined !== chart.realHeight ? chart.realHeight : chart.divRealHeight ) + 'px'; 450 curtain.style.textAlign = 'center'; 451 curtain.style.display = 'table'; 452 curtain.style.fontSize = '20px'; 453 try { 454 curtain.style.background = 'rgba(255, 255, 255, 0.3)'; 455 } catch ( e ) { 456 curtain.style.background = 'rgb(255, 255, 255)'; 457 } 458 curtain.innerHTML = '<div style="display: table-cell; vertical-align: middle;">' + msg + '</div>'; 459 } else { 460 curtain.innerHTML = msg; 461 } 462 chart.containerDiv.appendChild( curtain ); 463 464 l.curtain = curtain; 465 } 466 467 /** 468 * Removes the curtain 469 */ 470 function removeCurtain() { 471 try { 472 if ( undefined !== l.curtain ) 473 chart.containerDiv.removeChild( l.curtain ); 474 } catch ( e ) { 475 // do nothing 476 } 477 478 l.curtain = undefined; 479 480 } 481 482 /** 483 * Execute callback function 484 */ 485 function callFunction( func, param1, param2, param3 ) { 486 if ( 'function' === typeof func ) 487 func.call( l, param1, param2, param3 ); 488 } 489 490}, [ 'pie', 'serial', 'xy', 'funnel', 'radar', 'gauge', 'gantt', 'stock', 'map' ] ); 491 492 493/** 494 * Returns prompt in a chart language (set by chart.language) if it is 495 * available 496 */ 497if ( undefined === AmCharts.__ ) { 498 AmCharts.__ = function( msg, language ) { 499 if ( undefined !== language && undefined !== AmCharts.translations.dataLoader[ language ] && undefined !== AmCharts.translations.dataLoader[ language ][ msg ] ) 500 return AmCharts.translations.dataLoader[ language ][ msg ]; 501 else 502 return msg; 503 }; 504} 505 506/** 507 * Loads a file from url and calls function handler with the result 508 */ 509AmCharts.loadFile = function( url, options, handler ) { 510 511 // prepopulate options with minimal defaults if necessary 512 if ( typeof( options ) !== 'object' ) 513 options = {}; 514 if ( options.async === undefined ) 515 options.async = true; 516 517 // create the request 518 var request; 519 if ( window.XMLHttpRequest ) { 520 // IE7+, Firefox, Chrome, Opera, Safari 521 request = new XMLHttpRequest(); 522 } else { 523 // code for IE6, IE5 524 request = new ActiveXObject( 'Microsoft.XMLHTTP' ); 525 } 526 527 // open the connection 528 try { 529 request.open( 'GET', options.timestamp ? AmCharts.timestampUrl( url ) : url, options.async ); 530 } catch ( e ) { 531 handler.call( this, false ); 532 } 533 534 // add headers? 535 if ( options.headers !== undefined && options.headers.length ) { 536 for ( var i = 0; i < options.headers.length; i++ ) { 537 var header = options.headers[ i ]; 538 request.setRequestHeader( header.key, header.value ); 539 } 540 } 541 542 // add onprogress handlers 543 if ( options.progress !== undefined && typeof( options.progress ) === 'function' ) { 544 request.onprogress = function( e ) { 545 var complete = ( e.loaded / e.total ) * 100; 546 options.progress.call( this, complete ); 547 } 548 } 549 550 // set handler for data if async loading 551 request.onreadystatechange = function() { 552 553 if ( 4 === request.readyState && 404 === request.status ) 554 handler.call( this, false ); 555 556 else if ( 4 === request.readyState && 200 === request.status ) 557 handler.call( this, request.responseText ); 558 559 }; 560 561 // load the file 562 try { 563 request.send(); 564 } catch ( e ) { 565 handler.call( this, false ); 566 } 567 568}; 569 570/** 571 * Parses JSON string into an object 572 */ 573AmCharts.parseJSON = function( response ) { 574 try { 575 if ( undefined !== JSON ) 576 return JSON.parse( response ); 577 else 578 return eval( response ); 579 } catch ( e ) { 580 return false; 581 } 582}; 583 584/** 585 * Prases CSV string into an object 586 */ 587AmCharts.parseCSV = function( response, options ) { 588 589 // parse CSV into array 590 var data = AmCharts.CSVToArray( response, options.delimiter ); 591 592 // init resuling array 593 var res = []; 594 var cols = []; 595 var col, i; 596 597 // first row holds column names? 598 if ( options.useColumnNames ) { 599 cols = data.shift(); 600 601 // normalize column names 602 for ( var x = 0; x < cols.length; x++ ) { 603 // trim 604 col = cols[ x ].replace( /^\s+|\s+$/gm, '' ); 605 606 // check for empty 607 if ( '' === col ) 608 col = 'col' + x; 609 610 cols[ x ] = col; 611 } 612 613 if ( 0 < options.skip ) 614 options.skip--; 615 } 616 617 // skip rows 618 for ( i = 0; i < options.skip; i++ ) 619 data.shift(); 620 621 // iterate through the result set 622 var row; 623 while ( ( row = options.reverse ? data.pop() : data.shift() ) ) { 624 if ( options.skipEmpty && row.length === 1 && row[ 0 ] === '' ) 625 continue; 626 var dataPoint = {}; 627 for ( i = 0; i < row.length; i++ ) { 628 col = undefined === cols[ i ] ? 'col' + i : cols[ i ]; 629 dataPoint[ col ] = row[ i ] === "" ? options.emptyAs : row[ i ]; 630 } 631 res.push( dataPoint ); 632 } 633 634 return res; 635}; 636 637/** 638 * Parses CSV data into array 639 * Taken from here: (thanks!) 640 * http://www.bennadel.com/blog/1504-ask-ben-parsing-csv-strings-with-javascript-exec-regular-expression-command.htm 641 */ 642AmCharts.CSVToArray = function( strData, strDelimiter ) { 643 // Check to see if the delimiter is defined. If not, 644 // then default to comma. 645 strDelimiter = ( strDelimiter || ',' ); 646 647 // Create a regular expression to parse the CSV values. 648 var objPattern = new RegExp( 649 ( 650 // Delimiters. 651 "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + 652 653 // Quoted fields. 654 "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + 655 656 // Standard fields. 657 "([^\"\\" + strDelimiter + "\\r\\n]*))" 658 ), 659 "gi" 660 ); 661 662 663 // Create an array to hold our data. Give the array 664 // a default empty first row. 665 var arrData = [ 666 [] 667 ]; 668 669 // Create an array to hold our individual pattern 670 // matching groups. 671 var arrMatches = null; 672 673 674 // Keep looping over the regular expression matches 675 // until we can no longer find a match. 676 while ( ( arrMatches = objPattern.exec( strData ) ) ) { 677 678 // Get the delimiter that was found. 679 var strMatchedDelimiter = arrMatches[ 1 ]; 680 681 // Check to see if the given delimiter has a length 682 // (is not the start of string) and if it matches 683 // field delimiter. If id does not, then we know 684 // that this delimiter is a row delimiter. 685 if ( 686 strMatchedDelimiter.length && 687 ( strMatchedDelimiter !== strDelimiter ) 688 ) { 689 690 // Since we have reached a new row of data, 691 // add an empty row to our data array. 692 arrData.push( [] ); 693 694 } 695 696 697 // Now that we have our delimiter out of the way, 698 // let's check to see which kind of value we 699 // captured (quoted or unquoted). 700 var strMatchedValue; 701 if ( arrMatches[ 2 ] ) { 702 703 // We found a quoted value. When we capture 704 // this value, unescape any double quotes. 705 strMatchedValue = arrMatches[ 2 ].replace( 706 new RegExp( "\"\"", "g" ), 707 "\"" 708 ); 709 710 } else { 711 712 // We found a non-quoted value. 713 strMatchedValue = arrMatches[ 3 ]; 714 715 } 716 717 718 // Now that we have our value string, let's add 719 // it to the data array. 720 arrData[ arrData.length - 1 ].push( strMatchedValue ); 721 } 722 723 // Return the parsed data. 724 return ( arrData ); 725}; 726 727/** 728 * Appends timestamp to the url 729 */ 730AmCharts.timestampUrl = function( url ) { 731 var p = url.split( '?' ); 732 if ( 1 === p.length ) 733 p[ 1 ] = new Date().getTime(); 734 else 735 p[ 1 ] += '&' + new Date().getTime(); 736 return p.join( '?' ); 737};