geostats.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227
  1. /**
  2. * geostats() is a tiny and standalone javascript library for classification
  3. * Project page - https://github.com/simogeo/geostats
  4. * Copyright (c) 2011 Simon Georget, http://www.empreinte-urbaine.eu
  5. * Licensed under the MIT license
  6. */
  7. (function (definition) {
  8. // This file will function properly as a <script> tag, or a module
  9. // using CommonJS and NodeJS or RequireJS module formats.
  10. // CommonJS
  11. if (typeof exports === "object") {
  12. module.exports = definition();
  13. // RequireJS
  14. } else if (typeof define === "function" && define.amd) {
  15. define(definition);
  16. // <script>
  17. } else {
  18. geostats = definition();
  19. }
  20. })(function () {
  21. var isInt = function(n) {
  22. return typeof n === 'number' && parseFloat(n) == parseInt(n, 10) && !isNaN(n);
  23. } // 6 characters
  24. var _t = function(str) {
  25. return str;
  26. };
  27. //taking from http://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric
  28. var isNumber = function(n) {
  29. return !isNaN(parseFloat(n)) && isFinite(n);
  30. }
  31. //indexOf polyfill
  32. // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
  33. if (!Array.prototype.indexOf) {
  34. Array.prototype.indexOf = function (searchElement, fromIndex) {
  35. if ( this === undefined || this === null ) {
  36. throw new TypeError( '"this" is null or not defined' );
  37. }
  38. var length = this.length >>> 0; // Hack to convert object.length to a UInt32
  39. fromIndex = +fromIndex || 0;
  40. if (Math.abs(fromIndex) === Infinity) {
  41. fromIndex = 0;
  42. }
  43. if (fromIndex < 0) {
  44. fromIndex += length;
  45. if (fromIndex < 0) {
  46. fromIndex = 0;
  47. }
  48. }
  49. for (;fromIndex < length; fromIndex++) {
  50. if (this[fromIndex] === searchElement) {
  51. return fromIndex;
  52. }
  53. }
  54. return -1;
  55. };
  56. }
  57. var geostats = function(a) {
  58. this.objectID = '';
  59. this.separator = ' - ';
  60. this.legendSeparator = this.separator;
  61. this.method = '';
  62. this.precision = 0;
  63. this.precisionflag = 'auto';
  64. this.roundlength = 2; // Number of decimals, round values
  65. this.is_uniqueValues = false;
  66. this.debug = false;
  67. this.silent = false;
  68. this.bounds = Array();
  69. this.ranges = Array();
  70. this.inner_ranges = null;
  71. this.colors = Array();
  72. this.counter = Array();
  73. // statistics information
  74. this.stat_sorted = null;
  75. this.stat_mean = null;
  76. this.stat_median = null;
  77. this.stat_sum = null;
  78. this.stat_max = null;
  79. this.stat_min = null;
  80. this.stat_pop = null;
  81. this.stat_variance = null;
  82. this.stat_stddev = null;
  83. this.stat_cov = null;
  84. /**
  85. * logging method
  86. */
  87. this.log = function(msg, force) {
  88. if(this.debug == true || force != null)
  89. console.log(this.objectID + "(object id) :: " + msg);
  90. };
  91. /**
  92. * Set bounds
  93. */
  94. this.setBounds = function(a) {
  95. this.log('Setting bounds (' + a.length + ') : ' + a.join());
  96. this.bounds = Array() // init empty array to prevent bug when calling classification after another with less items (sample getQuantile(6) and getQuantile(4))
  97. this.bounds = a;
  98. //this.bounds = this.decimalFormat(a);
  99. };
  100. /**
  101. * Set a new serie
  102. */
  103. this.setSerie = function(a) {
  104. this.log('Setting serie (' + a.length + ') : ' + a.join());
  105. this.serie = Array() // init empty array to prevent bug when calling classification after another with less items (sample getQuantile(6) and getQuantile(4))
  106. this.serie = a;
  107. //reset statistics after changing serie
  108. this.resetStatistics();
  109. this.setPrecision();
  110. };
  111. /**
  112. * Set colors
  113. */
  114. this.setColors = function(colors) {
  115. this.log('Setting color ramp (' + colors.length + ') : ' + colors.join());
  116. this.colors = colors;
  117. };
  118. /**
  119. * Get feature count
  120. * With bounds array(0, 0.75, 1.5, 2.25, 3);
  121. * should populate this.counter with 5 keys
  122. * and increment counters for each key
  123. */
  124. this.doCount = function() {
  125. if (this._nodata())
  126. return;
  127. var tmp = this.sorted();
  128. this.counter = new Array();
  129. // we init counter with 0 value
  130. for(i = 0; i < this.bounds.length -1; i++) {
  131. this.counter[i]= 0;
  132. }
  133. for(j=0; j < tmp.length; j++) {
  134. // get current class for value to increment the counter
  135. var cclass = this.getClass(tmp[j]);
  136. this.counter[cclass]++;
  137. }
  138. };
  139. /**
  140. * Set decimal precision according to user input
  141. * or automatcally determined according
  142. * to the given serie.
  143. */
  144. this.setPrecision = function(decimals) {
  145. // only when called from user
  146. if(typeof decimals !== "undefined") {
  147. this.precisionflag = 'manual';
  148. this.precision = decimals;
  149. }
  150. // we calculate the maximal decimal length on given serie
  151. if(this.precisionflag == 'auto') {
  152. for (var i = 0; i < this.serie.length; i++) {
  153. // check if the given value is a number and a float
  154. if (!isNaN((this.serie[i]+"")) && (this.serie[i]+"").toString().indexOf('.') != -1) {
  155. var precision = (this.serie[i] + "").split(".")[1].length;
  156. } else {
  157. var precision = 0;
  158. }
  159. if(precision > this.precision) {
  160. this.precision = precision;
  161. }
  162. }
  163. }
  164. if(this.precision > 20) {
  165. // prevent "Uncaught RangeError: toFixed() digits argument must be between 0 and 20" bug. See https://github.com/simogeo/geostats/issues/34
  166. this.log('this.precision value (' + this.precision + ') is greater than max value. Automatic set-up to 20 to prevent "Uncaught RangeError: toFixed()" when calling decimalFormat() method.');
  167. this.precision = 20;
  168. }
  169. this.log('Calling setPrecision(). Mode : ' + this.precisionflag + ' - Decimals : '+ this.precision);
  170. this.serie = this.decimalFormat(this.serie);
  171. };
  172. /**
  173. * Format array numbers regarding to precision
  174. */
  175. this.decimalFormat = function(a) {
  176. var b = new Array();
  177. for (var i = 0; i < a.length; i++) {
  178. // check if the given value is a number
  179. if (isNumber(a[i])) {
  180. b[i] = parseFloat(parseFloat(a[i]).toFixed(this.precision));
  181. } else {
  182. b[i] = a[i];
  183. }
  184. }
  185. return b;
  186. }
  187. /**
  188. * Transform a bounds array to a range array the following array : array(0,
  189. * 0.75, 1.5, 2.25, 3); becomes : array('0-0.75', '0.75-1.5', '1.5-2.25',
  190. * '2.25-3');
  191. */
  192. this.setRanges = function() {
  193. this.ranges = Array(); // init empty array to prevent bug when calling classification after another with less items (sample getQuantile(6) and getQuantile(4))
  194. for (i = 0; i < (this.bounds.length - 1); i++) {
  195. this.ranges[i] = this.bounds[i] + this.separator + this.bounds[i + 1];
  196. }
  197. };
  198. /** return min value */
  199. this.min = function() {
  200. if (this._nodata())
  201. return;
  202. this.stat_min = this.serie[0];
  203. for (i = 0; i < this.pop(); i++) {
  204. if (this.serie[i] < this.stat_min) {
  205. this.stat_min = this.serie[i];
  206. }
  207. }
  208. return this.stat_min;
  209. };
  210. /** return max value */
  211. this.max = function() {
  212. if (this._nodata())
  213. return;
  214. this.stat_max = this.serie[0];
  215. for (i = 0; i < this.pop(); i++) {
  216. if (this.serie[i] > this.stat_max) {
  217. this.stat_max = this.serie[i];
  218. }
  219. }
  220. return this.stat_max;
  221. };
  222. /** return sum value */
  223. this.sum = function() {
  224. if (this._nodata())
  225. return;
  226. if (this.stat_sum == null) {
  227. this.stat_sum = 0;
  228. for (i = 0; i < this.pop(); i++) {
  229. this.stat_sum += parseFloat(this.serie[i]);
  230. }
  231. }
  232. return this.stat_sum;
  233. };
  234. /** return population number */
  235. this.pop = function() {
  236. if (this._nodata())
  237. return;
  238. if (this.stat_pop == null) {
  239. this.stat_pop = this.serie.length;
  240. }
  241. return this.stat_pop;
  242. };
  243. /** return mean value */
  244. this.mean = function() {
  245. if (this._nodata())
  246. return;
  247. if (this.stat_mean == null) {
  248. this.stat_mean = parseFloat(this.sum() / this.pop());
  249. }
  250. return this.stat_mean;
  251. };
  252. /** return median value */
  253. this.median = function() {
  254. if (this._nodata())
  255. return;
  256. if (this.stat_median == null) {
  257. this.stat_median = 0;
  258. var tmp = this.sorted();
  259. // serie pop is odd
  260. if (tmp.length % 2) {
  261. this.stat_median = parseFloat(tmp[(Math.ceil(tmp.length / 2) - 1)]);
  262. // serie pop is even
  263. } else {
  264. this.stat_median = ( parseFloat(tmp[((tmp.length / 2) - 1)]) + parseFloat(tmp[(tmp.length / 2)]) ) / 2;
  265. }
  266. }
  267. return this.stat_median;
  268. };
  269. /** return variance value */
  270. this.variance = function() {
  271. round = (typeof round === "undefined") ? true : false;
  272. if (this._nodata())
  273. return;
  274. if (this.stat_variance == null) {
  275. var tmp = 0, serie_mean = this.mean();
  276. for (var i = 0; i < this.pop(); i++) {
  277. tmp += Math.pow( (this.serie[i] - serie_mean), 2 );
  278. }
  279. this.stat_variance = tmp / this.pop();
  280. if(round == true) {
  281. this.stat_variance = Math.round(this.stat_variance * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
  282. }
  283. }
  284. return this.stat_variance;
  285. };
  286. /** return standard deviation value */
  287. this.stddev = function(round) {
  288. round = (typeof round === "undefined") ? true : false;
  289. if (this._nodata())
  290. return;
  291. if (this.stat_stddev == null) {
  292. this.stat_stddev = Math.sqrt(this.variance());
  293. if(round == true) {
  294. this.stat_stddev = Math.round(this.stat_stddev * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
  295. }
  296. }
  297. return this.stat_stddev;
  298. };
  299. /** coefficient of variation - measure of dispersion */
  300. this.cov = function(round) {
  301. round = (typeof round === "undefined") ? true : false;
  302. if (this._nodata())
  303. return;
  304. if (this.stat_cov == null) {
  305. this.stat_cov = this.stddev() / this.mean();
  306. if(round == true) {
  307. this.stat_cov = Math.round(this.stat_cov * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
  308. }
  309. }
  310. return this.stat_cov;
  311. };
  312. /** reset all attributes after setting a new serie */
  313. this.resetStatistics = function() {
  314. this.stat_sorted = null;
  315. this.stat_mean = null;
  316. this.stat_median = null;
  317. this.stat_sum = null;
  318. this.stat_max = null;
  319. this.stat_min = null;
  320. this.stat_pop = null;
  321. this.stat_variance = null;
  322. this.stat_stddev = null;
  323. this.stat_cov = null;
  324. }
  325. /** data test */
  326. this._nodata = function() {
  327. if (this.serie.length == 0) {
  328. if(this.silent) this.log("[silent mode] Error. You should first enter a serie!", true);
  329. else throw new TypeError("Error. You should first enter a serie!");
  330. return 1;
  331. } else
  332. return 0;
  333. };
  334. /** check if the serie contains negative value */
  335. this._hasNegativeValue = function() {
  336. for (i = 0; i < this.serie.length; i++) {
  337. if(this.serie[i] < 0)
  338. return true;
  339. }
  340. return false;
  341. };
  342. /** check if the serie contains zero value */
  343. this._hasZeroValue = function() {
  344. for (i = 0; i < this.serie.length; i++) {
  345. if(parseFloat(this.serie[i]) === 0)
  346. return true;
  347. }
  348. return false;
  349. };
  350. /** return sorted values (as array) */
  351. this.sorted = function() {
  352. if (this.stat_sorted == null) {
  353. if(this.is_uniqueValues == false) {
  354. this.stat_sorted = this.serie.sort(function(a, b) {
  355. return a - b;
  356. });
  357. } else {
  358. this.stat_sorted = this.serie.sort(function(a,b){
  359. var nameA=a.toString().toLowerCase(), nameB=b.toString().toLowerCase();
  360. if(nameA < nameB) return -1;
  361. if(nameA > nameB) return 1;
  362. return 0;
  363. })
  364. }
  365. }
  366. return this.stat_sorted;
  367. };
  368. /** return all info */
  369. this.info = function() {
  370. if (this._nodata())
  371. return;
  372. var content = '';
  373. content += _t('Population') + ' : ' + this.pop() + ' - [' + _t('Min')
  374. + ' : ' + this.min() + ' | ' + _t('Max') + ' : ' + this.max()
  375. + ']' + "\n";
  376. content += _t('Mean') + ' : ' + this.mean() + ' - ' + _t('Median') + ' : ' + this.median() + "\n";
  377. content += _t('Variance') + ' : ' + this.variance() + ' - ' + _t('Standard deviation') + ' : ' + this.stddev()
  378. + ' - ' + _t('Coefficient of variation') + ' : ' + this.cov() + "\n";
  379. return content;
  380. };
  381. /**
  382. * Set Manual classification Return an array with bounds : ie array(0,
  383. * 0.75, 1.5, 2.25, 3);
  384. * Set ranges and prepare data for displaying legend
  385. *
  386. */
  387. this.setClassManually = function(array) {
  388. if (this._nodata())
  389. return;
  390. if(array[0] !== this.min() || array[array.length-1] !== this.max()) {
  391. if(this.silent) this.log("[silent mode] " + t('Given bounds may not be correct! please check your input.\nMin value : ' + this.min() + ' / Max value : ' + this.max()), true);
  392. else throw new TypeError(_t('Given bounds may not be correct! please check your input.\nMin value : ' + this.min() + ' / Max value : ' + this.max()));
  393. return;
  394. }
  395. this.setBounds(array);
  396. this.setRanges();
  397. // we specify the classification method
  398. this.method = _t('manual classification') + ' (' + (array.length -1) + ' ' + _t('classes') + ')';
  399. return this.bounds;
  400. };
  401. /**
  402. * Equal intervals classification Return an array with bounds : ie array(0,
  403. * 0.75, 1.5, 2.25, 3);
  404. */
  405. this.getClassEqInterval = function(nbClass, forceMin, forceMax) {
  406. if (this._nodata())
  407. return;
  408. var tmpMin = (typeof forceMin === "undefined") ? this.min() : forceMin;
  409. var tmpMax = (typeof forceMax === "undefined") ? this.max() : forceMax;
  410. var a = Array();
  411. var val = tmpMin;
  412. var interval = (tmpMax - tmpMin) / nbClass;
  413. for (i = 0; i <= nbClass; i++) {
  414. a[i] = val;
  415. val += interval;
  416. }
  417. //-> Fix last bound to Max of values
  418. a[nbClass] = tmpMax;
  419. this.setBounds(a);
  420. this.setRanges();
  421. // we specify the classification method
  422. this.method = _t('eq. intervals') + ' (' + nbClass + ' ' + _t('classes') + ')';
  423. return this.bounds;
  424. };
  425. this.getQuantiles = function(nbClass) {
  426. var tmp = this.sorted();
  427. var quantiles = [];
  428. var step = this.pop() / nbClass;
  429. for (var i = 1; i < nbClass; i++) {
  430. var qidx = Math.round(i*step+0.49);
  431. quantiles.push(tmp[qidx-1]); // zero-based
  432. }
  433. return quantiles;
  434. };
  435. /**
  436. * Quantile classification Return an array with bounds : ie array(0, 0.75,
  437. * 1.5, 2.25, 3);
  438. */
  439. this.getClassQuantile = function(nbClass) {
  440. if (this._nodata())
  441. return;
  442. var tmp = this.sorted();
  443. var bounds = this.getQuantiles(nbClass);
  444. bounds.unshift(tmp[0]);
  445. if (bounds[tmp.length - 1] !== tmp[tmp.length - 1])
  446. bounds.push(tmp[tmp.length - 1]);
  447. this.setBounds(bounds);
  448. this.setRanges();
  449. // we specify the classification method
  450. this.method = _t('quantile') + ' (' + nbClass + ' ' + _t('classes') + ')';
  451. return this.bounds;
  452. };
  453. /**
  454. * Standard Deviation classification
  455. * Return an array with bounds : ie array(0,
  456. * 0.75, 1.5, 2.25, 3);
  457. */
  458. this.getClassStdDeviation = function(nbClass, matchBounds) {
  459. if (this._nodata())
  460. return;
  461. var tmpMax = this.max();
  462. var tmpMin = this.min();
  463. var a = Array();
  464. // number of classes is odd
  465. if(nbClass % 2 == 1) {
  466. // Euclidean division to get the inferior bound
  467. var infBound = Math.floor(nbClass / 2);
  468. var supBound = infBound + 1;
  469. // we set the central bounds
  470. a[infBound] = this.mean() - ( this.stddev() / 2);
  471. a[supBound] = this.mean() + ( this.stddev() / 2);
  472. // Values < to infBound, except first one
  473. for (i = infBound - 1; i > 0; i--) {
  474. var val = a[i+1] - this.stddev();
  475. a[i] = val;
  476. }
  477. // Values > to supBound, except last one
  478. for (i = supBound + 1; i < nbClass; i++) {
  479. var val = a[i-1] + this.stddev();
  480. a[i] = val;
  481. }
  482. // number of classes is even
  483. } else {
  484. var meanBound = nbClass / 2;
  485. // we get the mean value
  486. a[meanBound] = this.mean();
  487. // Values < to the mean, except first one
  488. for (i = meanBound - 1; i > 0; i--) {
  489. var val = a[i+1] - this.stddev();
  490. a[i] = val;
  491. }
  492. // Values > to the mean, except last one
  493. for (i = meanBound + 1; i < nbClass; i++) {
  494. var val = a[i-1] + this.stddev();
  495. a[i] = val;
  496. }
  497. }
  498. // we finally set the first value
  499. // do we excatly match min value or not ?
  500. a[0] = (typeof matchBounds === "undefined") ? a[1]-this.stddev() : this.min();
  501. // we finally set the last value
  502. // do we excatly match max value or not ?
  503. a[nbClass] = (typeof matchBounds === "undefined") ? a[nbClass-1]+this.stddev() : this.max();
  504. this.setBounds(a);
  505. this.setRanges();
  506. // we specify the classification method
  507. this.method = _t('std deviation') + ' (' + nbClass + ' ' + _t('classes')+ ')';
  508. return this.bounds;
  509. };
  510. /**
  511. * Geometric Progression classification
  512. * http://en.wikipedia.org/wiki/Geometric_progression
  513. * Return an array with bounds : ie array(0,
  514. * 0.75, 1.5, 2.25, 3);
  515. */
  516. this.getClassGeometricProgression = function(nbClass) {
  517. if (this._nodata())
  518. return;
  519. if(this._hasNegativeValue() || this._hasZeroValue()) {
  520. if(this.silent) this.log("[silent mode] " + _t('geometric progression can\'t be applied with a serie containing negative or zero values.'), true);
  521. else throw new TypeError(_t('geometric progression can\'t be applied with a serie containing negative or zero values.'));
  522. return;
  523. }
  524. var a = Array();
  525. var tmpMin = this.min();
  526. var tmpMax = this.max();
  527. var logMax = Math.log(tmpMax) / Math.LN10; // max decimal logarithm (or base 10)
  528. var logMin = Math.log(tmpMin) / Math.LN10;; // min decimal logarithm (or base 10)
  529. var interval = (logMax - logMin) / nbClass;
  530. // we compute log bounds
  531. for (i = 0; i < nbClass; i++) {
  532. if(i == 0) {
  533. a[i] = logMin;
  534. } else {
  535. a[i] = a[i-1] + interval;
  536. }
  537. }
  538. // we compute antilog
  539. a = a.map(function(x) { return Math.pow(10, x); });
  540. // and we finally add max value
  541. a.push(this.max());
  542. this.setBounds(a);
  543. this.setRanges();
  544. // we specify the classification method
  545. this.method = _t('geometric progression') + ' (' + nbClass + ' ' + _t('classes') + ')';
  546. return this.bounds;
  547. };
  548. /**
  549. * Arithmetic Progression classification
  550. * http://en.wikipedia.org/wiki/Arithmetic_progression
  551. * Return an array with bounds : ie array(0,
  552. * 0.75, 1.5, 2.25, 3);
  553. */
  554. this.getClassArithmeticProgression = function(nbClass) {
  555. if (this._nodata())
  556. return;
  557. var denominator = 0;
  558. // we compute the (french) "Raison"
  559. for (i = 1; i <= nbClass; i++) {
  560. denominator += i;
  561. }
  562. var a = Array();
  563. var tmpMin = this.min();
  564. var tmpMax = this.max();
  565. var interval = (tmpMax - tmpMin) / denominator;
  566. for (i = 0; i <= nbClass; i++) {
  567. if(i == 0) {
  568. a[i] = tmpMin;
  569. } else {
  570. a[i] = a[i-1] + (i * interval);
  571. }
  572. }
  573. this.setBounds(a);
  574. this.setRanges();
  575. // we specify the classification method
  576. this.method = _t('arithmetic progression') + ' (' + nbClass + ' ' + _t('classes') + ')';
  577. return this.bounds;
  578. };
  579. /**
  580. * Credits : Doug Curl (javascript) and Daniel J Lewis (python implementation)
  581. * http://www.arcgis.com/home/item.html?id=0b633ff2f40d412995b8be377211c47b
  582. * http://danieljlewis.org/2010/06/07/jenks-natural-breaks-algorithm-in-python/
  583. */
  584. this.getClassJenks = function(nbClass) {
  585. if (this._nodata())
  586. return;
  587. dataList = this.sorted();
  588. // now iterate through the datalist:
  589. // determine mat1 and mat2
  590. // really not sure how these 2 different arrays are set - the code for
  591. // each seems the same!
  592. // but the effect are 2 different arrays: mat1 and mat2
  593. var mat1 = []
  594. for ( var x = 0, xl = dataList.length + 1; x < xl; x++) {
  595. var temp = []
  596. for ( var j = 0, jl = nbClass + 1; j < jl; j++) {
  597. temp.push(0)
  598. }
  599. mat1.push(temp)
  600. }
  601. var mat2 = []
  602. for ( var i = 0, il = dataList.length + 1; i < il; i++) {
  603. var temp2 = []
  604. for ( var c = 0, cl = nbClass + 1; c < cl; c++) {
  605. temp2.push(0)
  606. }
  607. mat2.push(temp2)
  608. }
  609. // absolutely no idea what this does - best I can tell, it sets the 1st
  610. // group in the
  611. // mat1 and mat2 arrays to 1 and 0 respectively
  612. for ( var y = 1, yl = nbClass + 1; y < yl; y++) {
  613. mat1[0][y] = 1
  614. mat2[0][y] = 0
  615. for ( var t = 1, tl = dataList.length + 1; t < tl; t++) {
  616. mat2[t][y] = Infinity
  617. }
  618. var v = 0.0
  619. }
  620. // and this part - I'm a little clueless on - but it works
  621. // pretty sure it iterates across the entire dataset and compares each
  622. // value to
  623. // one another to and adjust the indices until you meet the rules:
  624. // minimum deviation
  625. // within a class and maximum separation between classes
  626. for ( var l = 2, ll = dataList.length + 1; l < ll; l++) {
  627. var s1 = 0.0
  628. var s2 = 0.0
  629. var w = 0.0
  630. for ( var m = 1, ml = l + 1; m < ml; m++) {
  631. var i3 = l - m + 1
  632. var val = parseFloat(dataList[i3 - 1])
  633. s2 += val * val
  634. s1 += val
  635. w += 1
  636. v = s2 - (s1 * s1) / w
  637. var i4 = i3 - 1
  638. if (i4 != 0) {
  639. for ( var p = 2, pl = nbClass + 1; p < pl; p++) {
  640. if (mat2[l][p] >= (v + mat2[i4][p - 1])) {
  641. mat1[l][p] = i3
  642. mat2[l][p] = v + mat2[i4][p - 1]
  643. }
  644. }
  645. }
  646. }
  647. mat1[l][1] = 1
  648. mat2[l][1] = v
  649. }
  650. var k = dataList.length
  651. var kclass = []
  652. // fill the kclass (classification) array with zeros:
  653. for (i = 0; i <= nbClass; i++) {
  654. kclass.push(0);
  655. }
  656. // this is the last number in the array:
  657. kclass[nbClass] = parseFloat(dataList[dataList.length - 1])
  658. // this is the first number - can set to zero, but want to set to lowest
  659. // to use for legend:
  660. kclass[0] = parseFloat(dataList[0])
  661. var countNum = nbClass
  662. while (countNum >= 2) {
  663. var id = parseInt((mat1[k][countNum]) - 2)
  664. kclass[countNum - 1] = dataList[id]
  665. k = parseInt((mat1[k][countNum] - 1))
  666. // spits out the rank and value of the break values:
  667. // console.log("id="+id,"rank = " + String(mat1[k][countNum]),"val =
  668. // " + String(dataList[id]))
  669. // count down:
  670. countNum -= 1
  671. }
  672. // check to see if the 0 and 1 in the array are the same - if so, set 0
  673. // to 0:
  674. if (kclass[0] == kclass[1]) {
  675. kclass[0] = 0
  676. }
  677. this.setBounds(kclass);
  678. this.setRanges();
  679. this.method = _t('Jenks') + ' (' + nbClass + ' ' + _t('classes') + ')';
  680. return this.bounds; //array of breaks
  681. }
  682. /**
  683. * Quantile classification Return an array with bounds : ie array(0, 0.75,
  684. * 1.5, 2.25, 3);
  685. */
  686. this.getClassUniqueValues = function() {
  687. if (this._nodata())
  688. return;
  689. this.is_uniqueValues = true;
  690. var tmp = this.sorted(); // display in alphabetical order
  691. var a = Array();
  692. for (i = 0; i < this.pop(); i++) {
  693. if(a.indexOf(tmp[i]) === -1)
  694. a.push(tmp[i]);
  695. }
  696. this.bounds = a;
  697. // we specify the classification method
  698. this.method = _t('unique values');
  699. return a;
  700. };
  701. /**
  702. * Return the class of a given value.
  703. * For example value : 6
  704. * and bounds array = (0, 4, 8, 12);
  705. * Return 2
  706. */
  707. this.getClass = function(value) {
  708. for(i = 0; i < this.bounds.length; i++) {
  709. if(this.is_uniqueValues == true) {
  710. if(value == this.bounds[i])
  711. return i;
  712. } else {
  713. // parseFloat() is necessary
  714. if(parseFloat(value) <= this.bounds[i + 1]) {
  715. return i;
  716. }
  717. }
  718. }
  719. return _t("Unable to get value's class.");
  720. };
  721. /**
  722. * Return the ranges array : array('0-0.75', '0.75-1.5', '1.5-2.25',
  723. * '2.25-3');
  724. */
  725. this.getRanges = function() {
  726. return this.ranges;
  727. };
  728. /**
  729. * Returns the number/index of this.ranges that value falls into
  730. */
  731. this.getRangeNum = function(value) {
  732. var bounds, i;
  733. for (i = 0; i < this.ranges.length; i++) {
  734. bounds = this.ranges[i].split(/ - /);
  735. if (value <= parseFloat(bounds[1])) {
  736. return i;
  737. }
  738. }
  739. }
  740. /*
  741. * Compute inner ranges based on serie.
  742. * Produce discontinous ranges used for legend - return an array similar to :
  743. * array('0.00-0.74', '0.98-1.52', '1.78-2.25', '2.99-3.14');
  744. * If inner ranges already computed, return array values.
  745. */
  746. this.getInnerRanges = function() {
  747. // if already computed, we return the result
  748. if(this.inner_ranges != null)
  749. return this.inner_ranges;
  750. var a = new Array();
  751. var tmp = this.sorted();
  752. var cnt = 1; // bounds array counter
  753. for (i = 0; i < tmp.length; i++) {
  754. if(i == 0) var range_firstvalue = tmp[i]; // we init first range value
  755. if(parseFloat(tmp[i]) > parseFloat(this.bounds[cnt])) {
  756. a[cnt - 1] = '' + range_firstvalue + this.separator + tmp[i-1];
  757. var range_firstvalue = tmp[i];
  758. cnt++;
  759. }
  760. // we reach the last range, we finally complete manually
  761. // and return the array
  762. if(cnt == (this.bounds.length - 1)) {
  763. // we set the last value
  764. a[cnt - 1] = '' + range_firstvalue + this.separator + tmp[tmp.length-1];
  765. this.inner_ranges = a;
  766. return this.inner_ranges;
  767. }
  768. }
  769. };
  770. this.getSortedlist = function() {
  771. return this.sorted().join(', ');
  772. };
  773. /**
  774. * Return an html legend
  775. * colors : specify an array of color (hexadecimal values)
  776. * legend : specify a text input for the legend. By default, just displays 'legend'
  777. * counter : if not null, display counter value
  778. * callback : if not null, callback function applied on legend boundaries
  779. * mode : null, 'default', 'distinct', 'discontinuous' :
  780. * - if mode is null, will display legend as 'default mode'
  781. * - 'default' : displays ranges like in ranges array (continuous values), sample : 29.26 - 378.80 / 378.80 - 2762.25 / 2762.25 - 6884.84
  782. * - 'distinct' : Add + 1 according to decimal precision to distinguish classes (discrete values), sample : 29.26 - 378.80 / 378.81 - 2762.25 / 2762.26 - 6884.84
  783. * - 'discontinuous' : indicates the range of data actually falling in each class , sample : 29.26 - 225.43 / 852.12 - 2762.20 / 3001.25 - 6884.84 / not implemented yet
  784. * order : null, 'ASC', 'DESC'
  785. */
  786. this.getHtmlLegend = function(colors, legend, counter, callback, mode, order) {
  787. var cnt= '';
  788. var elements = new Array();
  789. this.doCount(); // we do count, even if not displayed
  790. if(colors != null) {
  791. ccolors = colors;
  792. }
  793. else {
  794. ccolors = this.colors;
  795. }
  796. if(legend != null) {
  797. lg = legend;
  798. }
  799. else {
  800. lg = 'Legend';
  801. }
  802. if(counter != null) {
  803. getcounter = true;
  804. }
  805. else {
  806. getcounter = false;
  807. }
  808. if(callback != null) {
  809. fn = callback;
  810. }
  811. else {
  812. fn = function(o) {return o;};
  813. }
  814. if(mode == null) {
  815. mode = 'default';
  816. }
  817. if(mode == 'discontinuous') {
  818. this.getInnerRanges();
  819. // check if some classes are not populated / equivalent of in_array function
  820. if(this.counter.indexOf(0) !== -1) {
  821. if(this.silent) this.log("[silent mode] " + _t("Geostats cannot apply 'discontinuous' mode to the getHtmlLegend() method because some classes are not populated.\nPlease switch to 'default' or 'distinct' modes. Exit!"), true);
  822. else throw new TypeError(_t("Geostats cannot apply 'discontinuous' mode to the getHtmlLegend() method because some classes are not populated.\nPlease switch to 'default' or 'distinct' modes. Exit!"));
  823. return;
  824. }
  825. }
  826. if(order !== 'DESC') order = 'ASC';
  827. if(ccolors.length < this.ranges.length) {
  828. if(this.silent) this.log("[silent mode] " + _t('The number of colors should fit the number of ranges. Exit!'), true);
  829. else throw new TypeError(_t('The number of colors should fit the number of ranges. Exit!'));
  830. return;
  831. }
  832. if(this.is_uniqueValues == false) {
  833. for (i = 0; i < (this.ranges.length); i++) {
  834. if(getcounter===true) {
  835. cnt = ' <span class="geostats-legend-counter">(' + this.counter[i] + ')</span>';
  836. }
  837. //console.log("Ranges : " + this.ranges[i]);
  838. // default mode
  839. var tmp = this.ranges[i].split(this.separator);
  840. var start_value = parseFloat(tmp[0]).toFixed(this.precision);
  841. var end_value = parseFloat(tmp[1]).toFixed(this.precision);
  842. // if mode == 'distinct' and we are not working on the first value
  843. if(mode == 'distinct' && i != 0) {
  844. if(isInt(start_value)) {
  845. start_value = parseInt(start_value) + 1;
  846. // format to float if necessary
  847. if(this.precisionflag == 'manual' && this.precision != 0) start_value = parseFloat(start_value).toFixed(this.precision);
  848. } else {
  849. start_value = parseFloat(start_value) + (1 / Math.pow(10,this.precision));
  850. // strangely the formula above return sometimes long decimal values,
  851. // the following instruction fix it
  852. start_value = parseFloat(start_value).toFixed(this.precision);
  853. }
  854. }
  855. // if mode == 'discontinuous'
  856. if(mode == 'discontinuous') {
  857. var tmp = this.inner_ranges[i].split(this.separator);
  858. // console.log("Ranges : " + this.inner_ranges[i]);
  859. var start_value = parseFloat(tmp[0]).toFixed(this.precision);
  860. var end_value = parseFloat(tmp[1]).toFixed(this.precision);
  861. }
  862. // we apply callback function
  863. var el = fn(start_value) + this.legendSeparator + fn(end_value);
  864. var block = '<div><div class="geostats-legend-block" style="background-color:' + ccolors[i] + '"></div> ' + el + cnt + '</div>';
  865. elements.push(block);
  866. }
  867. } else {
  868. // only if classification is done on unique values
  869. for (i = 0; i < (this.bounds.length); i++) {
  870. if(getcounter===true) {
  871. cnt = ' <span class="geostats-legend-counter">(' + this.counter[i] + ')</span>';
  872. }
  873. var el = fn(this.bounds[i]);
  874. var block = '<div><div class="geostats-legend-block" style="background-color:' + ccolors[i] + '"></div> ' + el + cnt + '</div>';
  875. elements.push(block);
  876. }
  877. }
  878. // do we reverse the return legend ?
  879. if(order === 'DESC') elements.reverse();
  880. // finally we create HTML and return it
  881. var content = '<div class="geostats-legend"><div class="geostats-legend-title">' + _t(lg) + '</div>';
  882. for (i = 0; i < (elements.length); i++) {
  883. content += elements[i];
  884. }
  885. content += '</div>';
  886. return content;
  887. };
  888. // object constructor
  889. // At the end of script. If not setPrecision() method is not known
  890. // we create an object identifier for debugging
  891. this.objectID = new Date().getUTCMilliseconds();
  892. this.log('Creating new geostats object');
  893. if(typeof a !== 'undefined' && a.length > 0) {
  894. this.serie = a;
  895. this.setPrecision();
  896. this.log('Setting serie (' + a.length + ') : ' + a.join());
  897. } else {
  898. this.serie = Array();
  899. };
  900. // creating aliases on classification function for backward compatibility
  901. this.getJenks = this.getClassJenks;
  902. this.getGeometricProgression = this.getClassGeometricProgression;
  903. this.getEqInterval = this.getClassEqInterval;
  904. this.getQuantile = this.getClassQuantile;
  905. this.getStdDeviation = this.getClassStdDeviation;
  906. this.getUniqueValues = this.getClassUniqueValues;
  907. this.getArithmeticProgression = this.getClassArithmeticProgression;
  908. };
  909. window.geostats = geostats;
  910. return geostats;
  911. });