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};