dataloader.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. /*
  2. Plugin Name: amCharts Data Loader
  3. Description: This plugin adds external data loading capabilities to all amCharts libraries.
  4. Author: Martynas Majeris, amCharts
  5. Version: 1.0.5
  6. Author URI: http://www.amcharts.com/
  7. Copyright 2015 amCharts
  8. Licensed under the Apache License, Version 2.0 (the "License");
  9. you may not use this file except in compliance with the License.
  10. You may obtain a copy of the License at
  11. http://www.apache.org/licenses/LICENSE-2.0
  12. Unless required by applicable law or agreed to in writing, software
  13. distributed under the License is distributed on an "AS IS" BASIS,
  14. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. See the License for the specific language governing permissions and
  16. limitations under the License.
  17. Please note that the above license covers only this plugin. It by all means does
  18. not apply to any other amCharts products that are covered by different licenses.
  19. */
  20. /**
  21. * TODO:
  22. * incremental load
  23. * XML support (?)
  24. */
  25. /* globals AmCharts, ActiveXObject */
  26. /* jshint -W061 */
  27. /**
  28. * Initialize language prompt container
  29. */
  30. AmCharts.translations.dataLoader = {};
  31. /**
  32. * Set init handler
  33. */
  34. AmCharts.addInitHandler( function( chart ) {
  35. /**
  36. * Check if dataLoader is set (initialize it)
  37. */
  38. if ( undefined === chart.dataLoader || !isObject( chart.dataLoader ) )
  39. chart.dataLoader = {};
  40. /**
  41. * Check charts version for compatibility:
  42. * the first compatible version is 3.13
  43. */
  44. var version = chart.version.split( '.' );
  45. if ( ( Number( version[ 0 ] ) < 3 ) || ( 3 === Number( version[ 0 ] ) && ( Number( version[ 1 ] ) < 13 ) ) )
  46. return;
  47. /**
  48. * Define object reference for easy access
  49. */
  50. var l = chart.dataLoader;
  51. l.remaining = 0;
  52. /**
  53. * Set defaults
  54. */
  55. var defaults = {
  56. 'async': true,
  57. 'format': 'json',
  58. 'showErrors': true,
  59. 'showCurtain': true,
  60. 'noStyles': false,
  61. 'reload': 0,
  62. 'timestamp': false,
  63. 'delimiter': ',',
  64. 'skip': 0,
  65. 'useColumnNames': false,
  66. 'reverse': false,
  67. 'reloading': false,
  68. 'complete': false,
  69. 'error': false
  70. };
  71. /**
  72. * Create a function that can be used to load data (or reload via API)
  73. */
  74. l.loadData = function() {
  75. /**
  76. * Load all files in a row
  77. */
  78. if ( 'stock' === chart.type ) {
  79. // delay this a little bit so the chart has the chance to build itself
  80. setTimeout( function() {
  81. // preserve animation
  82. if ( 0 > chart.panelsSettings.startDuration ) {
  83. l.startDuration = chart.panelsSettings.startDuration;
  84. chart.panelsSettings.startDuration = 0;
  85. }
  86. // cycle through all of the data sets
  87. for ( var x = 0; x < chart.dataSets.length; x++ ) {
  88. var ds = chart.dataSets[ x ];
  89. // load data
  90. if ( undefined !== ds.dataLoader && undefined !== ds.dataLoader.url ) {
  91. ds.dataProvider = [];
  92. applyDefaults( ds.dataLoader );
  93. loadFile( ds.dataLoader.url, ds, ds.dataLoader, 'dataProvider' );
  94. }
  95. // load events data
  96. if ( undefined !== ds.eventDataLoader && undefined !== ds.eventDataLoader.url ) {
  97. ds.events = [];
  98. applyDefaults( ds.eventDataLoader );
  99. loadFile( ds.eventDataLoader.url, ds, ds.eventDataLoader, 'stockEvents' );
  100. }
  101. }
  102. }, 100 );
  103. } else {
  104. applyDefaults( l );
  105. if ( undefined === l.url )
  106. return;
  107. // preserve animation
  108. if ( undefined !== chart.startDuration && ( 0 < chart.startDuration ) ) {
  109. l.startDuration = chart.startDuration;
  110. chart.startDuration = 0;
  111. }
  112. // set empty data set
  113. if ( undefined === chart.dataProvider )
  114. chart.dataProvider = chart.type === 'map' ? {} : [];
  115. loadFile( l.url, chart, l, 'dataProvider' );
  116. }
  117. };
  118. /**
  119. * Trigger load
  120. */
  121. l.loadData();
  122. /**
  123. * Loads a file and determines correct parsing mechanism for it
  124. */
  125. function loadFile( url, holder, options, providerKey ) {
  126. // set default providerKey
  127. if ( undefined === providerKey )
  128. providerKey = 'dataProvider';
  129. // show curtain
  130. if ( options.showCurtain )
  131. showCurtain( undefined, options.noStyles );
  132. // increment loader count
  133. l.remaining++;
  134. // load the file
  135. AmCharts.loadFile( url, options, function( response ) {
  136. // error?
  137. if ( false === response ) {
  138. callFunction( options.error, options, chart );
  139. raiseError( AmCharts.__( 'Error loading the file', chart.language ) + ': ' + url, false, options );
  140. } else {
  141. // determine the format
  142. if ( undefined === options.format ) {
  143. // TODO
  144. options.format = 'json';
  145. }
  146. // lowercase
  147. options.format = options.format.toLowerCase();
  148. // invoke parsing function
  149. switch ( options.format ) {
  150. case 'json':
  151. holder[ providerKey ] = AmCharts.parseJSON( response );
  152. if ( false === holder[ providerKey ] ) {
  153. callFunction( options.error, options, chart );
  154. raiseError( AmCharts.__( 'Error parsing JSON file', chart.language ) + ': ' + l.url, false, options );
  155. holder[ providerKey ] = [];
  156. return;
  157. } else {
  158. holder[ providerKey ] = postprocess( holder[ providerKey ], options );
  159. callFunction( options.load, options, chart );
  160. }
  161. break;
  162. case 'csv':
  163. holder[ providerKey ] = AmCharts.parseCSV( response, options );
  164. if ( false === holder[ providerKey ] ) {
  165. callFunction( options.error, options, chart );
  166. raiseError( AmCharts.__( 'Error parsing CSV file', chart.language ) + ': ' + l.url, false, options );
  167. holder[ providerKey ] = [];
  168. return;
  169. } else {
  170. holder[ providerKey ] = postprocess( holder[ providerKey ], options );
  171. callFunction( options.load, options, chart );
  172. }
  173. break;
  174. default:
  175. callFunction( options.error, options, chart );
  176. raiseError( AmCharts.__( 'Unsupported data format', chart.language ) + ': ' + options.format, false, options.noStyles );
  177. return;
  178. }
  179. // decrement remaining counter
  180. l.remaining--;
  181. // we done?
  182. if ( 0 === l.remaining ) {
  183. // callback
  184. callFunction( options.complete, chart );
  185. // take in the new data
  186. if ( options.async ) {
  187. if ( 'map' === chart.type )
  188. chart.validateNow( true );
  189. else {
  190. // take in new data
  191. chart.validateData();
  192. // make the chart animate again
  193. if ( l.startDuration ) {
  194. if ( 'stock' === chart.type ) {
  195. chart.panelsSettings.startDuration = l.startDuration;
  196. for ( var x = 0; x < chart.panels.length; x++ ) {
  197. chart.panels[ x ].startDuration = l.startDuration;
  198. chart.panels[ x ].animateAgain();
  199. }
  200. } else {
  201. chart.startDuration = l.startDuration;
  202. chart.animateAgain();
  203. }
  204. }
  205. }
  206. }
  207. // restore default period
  208. if ( 'stock' === chart.type && !options.reloading && undefined !== chart.periodSelector )
  209. chart.periodSelector.setDefaultPeriod();
  210. // remove curtain
  211. removeCurtain();
  212. }
  213. // schedule another load if necessary
  214. if ( options.reload ) {
  215. if ( options.timeout )
  216. clearTimeout( options.timeout );
  217. options.timeout = setTimeout( loadFile, 1000 * options.reload, url, holder, options );
  218. options.reloading = true;
  219. }
  220. }
  221. } );
  222. }
  223. /**
  224. * Checks if postProcess is set and invokes the handler
  225. */
  226. function postprocess( data, options ) {
  227. if ( undefined !== options.postProcess && isFunction( options.postProcess ) )
  228. try {
  229. return options.postProcess.call( this, data, options );
  230. } catch ( e ) {
  231. raiseError( AmCharts.__( 'Error loading file', chart.language ) + ': ' + options.url, false, options );
  232. return data;
  233. } else
  234. return data;
  235. }
  236. /**
  237. * Returns true if argument is array
  238. */
  239. function isObject( obj ) {
  240. return 'object' === typeof( obj );
  241. }
  242. /**
  243. * Returns true is argument is a function
  244. */
  245. function isFunction( obj ) {
  246. return 'function' === typeof( obj );
  247. }
  248. /**
  249. * Applies defaults to config object
  250. */
  251. function applyDefaults( obj ) {
  252. for ( var x in defaults ) {
  253. if ( defaults.hasOwnProperty( x ) )
  254. setDefault( obj, x, defaults[ x ] );
  255. }
  256. }
  257. /**
  258. * Checks if object property is set, sets with a default if it isn't
  259. */
  260. function setDefault( obj, key, value ) {
  261. if ( undefined === obj[ key ] )
  262. obj[ key ] = value;
  263. }
  264. /**
  265. * Raises an internal error (writes it out to console)
  266. */
  267. function raiseError( msg, error, options ) {
  268. if ( options.showErrors )
  269. showCurtain( msg, options.noStyles );
  270. else {
  271. removeCurtain();
  272. console.log( msg );
  273. }
  274. }
  275. /**
  276. * Shows curtain over chart area
  277. */
  278. function showCurtain( msg, noStyles ) {
  279. // remove previous curtain if there is one
  280. removeCurtain();
  281. // did we pass in the message?
  282. if ( undefined === msg )
  283. msg = AmCharts.__( 'Loading data...', chart.language );
  284. // create and populate curtain element
  285. var curtain = document.createElement( 'div' );
  286. curtain.setAttribute( 'id', chart.div.id + '-curtain' );
  287. curtain.className = 'amcharts-dataloader-curtain';
  288. if ( true !== noStyles ) {
  289. curtain.style.position = 'absolute';
  290. curtain.style.top = 0;
  291. curtain.style.left = 0;
  292. curtain.style.width = ( undefined !== chart.realWidth ? chart.realWidth : chart.divRealWidth ) + 'px';
  293. curtain.style.height = ( undefined !== chart.realHeight ? chart.realHeight : chart.divRealHeight ) + 'px';
  294. curtain.style.textAlign = 'center';
  295. curtain.style.display = 'table';
  296. curtain.style.fontSize = '20px';
  297. try {
  298. curtain.style.background = 'rgba(255, 255, 255, 0.3)';
  299. }
  300. catch ( e ) {
  301. curtain.style.background = 'rgb(255, 255, 255)';
  302. }
  303. curtain.innerHTML = '<div style="display: table-cell; vertical-align: middle;">' + msg + '</div>';
  304. } else {
  305. curtain.innerHTML = msg;
  306. }
  307. chart.containerDiv.appendChild( curtain );
  308. l.curtain = curtain;
  309. }
  310. /**
  311. * Removes the curtain
  312. */
  313. function removeCurtain() {
  314. try {
  315. if ( undefined !== l.curtain )
  316. chart.containerDiv.removeChild( l.curtain );
  317. } catch ( e ) {
  318. // do nothing
  319. }
  320. l.curtain = undefined;
  321. }
  322. /**
  323. * Execute callback function
  324. */
  325. function callFunction( func, param1, param2, param3 ) {
  326. if ( 'function' === typeof func )
  327. func.call( l, param1, param2, param3 );
  328. }
  329. }, [ 'pie', 'serial', 'xy', 'funnel', 'radar', 'gauge', 'gantt', 'stock', 'map' ] );
  330. /**
  331. * Returns prompt in a chart language (set by chart.language) if it is
  332. * available
  333. */
  334. if ( undefined === AmCharts.__ ) {
  335. AmCharts.__ = function( msg, language ) {
  336. if ( undefined !== language && undefined !== AmCharts.translations.dataLoader[ language ] && undefined !== AmCharts.translations.dataLoader[ language ][ msg ] )
  337. return AmCharts.translations.dataLoader[ language ][ msg ];
  338. else
  339. return msg;
  340. };
  341. }
  342. /**
  343. * Loads a file from url and calls function handler with the result
  344. */
  345. AmCharts.loadFile = function( url, options, handler ) {
  346. // create the request
  347. var request;
  348. if ( window.XMLHttpRequest ) {
  349. // IE7+, Firefox, Chrome, Opera, Safari
  350. request = new XMLHttpRequest();
  351. } else {
  352. // code for IE6, IE5
  353. request = new ActiveXObject( 'Microsoft.XMLHTTP' );
  354. }
  355. // set handler for data if async loading
  356. request.onreadystatechange = function() {
  357. if ( 4 === request.readyState && 404 === request.status )
  358. handler.call( this, false );
  359. else if ( 4 === request.readyState && 200 === request.status )
  360. handler.call( this, request.responseText );
  361. };
  362. // load the file
  363. try {
  364. request.open( 'GET', options.timestamp ? AmCharts.timestampUrl( url ) : url, options.async );
  365. request.send();
  366. } catch ( e ) {
  367. handler.call( this, false );
  368. }
  369. };
  370. /**
  371. * Parses JSON string into an object
  372. */
  373. AmCharts.parseJSON = function( response ) {
  374. try {
  375. if ( undefined !== JSON )
  376. return JSON.parse( response );
  377. else
  378. return eval( response );
  379. } catch ( e ) {
  380. return false;
  381. }
  382. };
  383. /**
  384. * Prases CSV string into an object
  385. */
  386. AmCharts.parseCSV = function( response, options ) {
  387. // parse CSV into array
  388. var data = AmCharts.CSVToArray( response, options.delimiter );
  389. // init resuling array
  390. var res = [];
  391. var cols = [];
  392. var col, i;
  393. // first row holds column names?
  394. if ( options.useColumnNames ) {
  395. cols = data.shift();
  396. // normalize column names
  397. for ( var x = 0; x < cols.length; x++ ) {
  398. // trim
  399. col = cols[ x ].replace( /^\s+|\s+$/gm, '' );
  400. // check for empty
  401. if ( '' === col )
  402. col = 'col' + x;
  403. cols[ x ] = col;
  404. }
  405. if ( 0 < options.skip )
  406. options.skip--;
  407. }
  408. // skip rows
  409. for ( i = 0; i < options.skip; i++ )
  410. data.shift();
  411. // iterate through the result set
  412. var row;
  413. while ( ( row = options.reverse ? data.pop() : data.shift() ) ) {
  414. var dataPoint = {};
  415. for ( i = 0; i < row.length; i++ ) {
  416. col = undefined === cols[ i ] ? 'col' + i : cols[ i ];
  417. dataPoint[ col ] = row[ i ];
  418. }
  419. res.push( dataPoint );
  420. }
  421. return res;
  422. };
  423. /**
  424. * Parses CSV data into array
  425. * Taken from here: (thanks!)
  426. * http://www.bennadel.com/blog/1504-ask-ben-parsing-csv-strings-with-javascript-exec-regular-expression-command.htm
  427. */
  428. AmCharts.CSVToArray = function( strData, strDelimiter ) {
  429. // Check to see if the delimiter is defined. If not,
  430. // then default to comma.
  431. strDelimiter = ( strDelimiter || "," );
  432. // Create a regular expression to parse the CSV values.
  433. var objPattern = new RegExp(
  434. (
  435. // Delimiters.
  436. "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
  437. // Quoted fields.
  438. "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
  439. // Standard fields.
  440. "([^\"\\" + strDelimiter + "\\r\\n]*))"
  441. ),
  442. "gi"
  443. );
  444. // Create an array to hold our data. Give the array
  445. // a default empty first row.
  446. var arrData = [
  447. []
  448. ];
  449. // Create an array to hold our individual pattern
  450. // matching groups.
  451. var arrMatches = null;
  452. // Keep looping over the regular expression matches
  453. // until we can no longer find a match.
  454. while ( ( arrMatches = objPattern.exec( strData ) ) ) {
  455. // Get the delimiter that was found.
  456. var strMatchedDelimiter = arrMatches[ 1 ];
  457. // Check to see if the given delimiter has a length
  458. // (is not the start of string) and if it matches
  459. // field delimiter. If id does not, then we know
  460. // that this delimiter is a row delimiter.
  461. if (
  462. strMatchedDelimiter.length &&
  463. ( strMatchedDelimiter !== strDelimiter )
  464. ) {
  465. // Since we have reached a new row of data,
  466. // add an empty row to our data array.
  467. arrData.push( [] );
  468. }
  469. // Now that we have our delimiter out of the way,
  470. // let's check to see which kind of value we
  471. // captured (quoted or unquoted).
  472. var strMatchedValue;
  473. if ( arrMatches[ 2 ] ) {
  474. // We found a quoted value. When we capture
  475. // this value, unescape any double quotes.
  476. strMatchedValue = arrMatches[ 2 ].replace(
  477. new RegExp( "\"\"", "g" ),
  478. "\""
  479. );
  480. } else {
  481. // We found a non-quoted value.
  482. strMatchedValue = arrMatches[ 3 ];
  483. }
  484. // Now that we have our value string, let's add
  485. // it to the data array.
  486. arrData[ arrData.length - 1 ].push( strMatchedValue );
  487. }
  488. // Return the parsed data.
  489. return ( arrData );
  490. };
  491. /**
  492. * Appends timestamp to the url
  493. */
  494. AmCharts.timestampUrl = function( url ) {
  495. var p = url.split( '?' );
  496. if ( 1 === p.length )
  497. p[ 1 ] = new Date().getTime();
  498. else
  499. p[ 1 ] += '&' + new Date().getTime();
  500. return p.join( '?' );
  501. };