blob: adc8e1dd31425dfc8acf2772e22a62c668f04a71 [file] [log] [blame]
genia.likes.science@gmail.coma362b4d2013-04-26 08:29:25 -07001/**
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://valums.com
5* Licensed under the MIT license
6*/
7
8var _t = function(str) {
9 return str;
10};
11
12var inArray = function(needle, haystack) {
13 for(var i = 0; i < haystack.length; i++) {
14 if(haystack[i] == needle) return true;
15 }
16 return false;
17};
18
19var geostats = function(a) {
20
21 this.separator = ' - ';
22 this.legendSeparator = this.separator;
23 this.method = '';
24 this.roundlength = 2; // Number of decimals, round values
25 this.is_uniqueValues = false;
26
27 this.bounds = Array();
28 this.ranges = Array();
29 this.colors = Array();
30 this.counter = Array();
31
32 // statistics information
33 this.stat_sorted = null;
34 this.stat_mean = null;
35 this.stat_median = null;
36 this.stat_sum = null;
37 this.stat_max = null;
38 this.stat_min = null;
39 this.stat_pop = null;
40 this.stat_variance = null;
41 this.stat_stddev = null;
42 this.stat_cov = null;
43
44
45 if(typeof a !== 'undefined' && a.length > 0) {
46 this.serie = a;
47 } else {
48 this.serie = Array();
49 };
50
51 /**
52 * Set a new serie
53 */
54 this.setSerie = function(a) {
55
56 this.serie = Array() // init empty array to prevent bug when calling classification after another with less items (sample getQuantile(6) and getQuantile(4))
57 this.serie = a;
58
59 };
60
61 /**
62 * Set colors
63 */
64 this.setColors = function(colors) {
65
66 this.colors = colors;
67
68 };
69
70 /**
71 * Get feature count
72 * With bounds array(0, 0.75, 1.5, 2.25, 3);
73 * should populate this.counter with 5 keys
74 * and increment counters for each key
75 */
76 this.doCount = function() {
77
78 if (this._nodata())
79 return;
80
81
82 var tmp = this.sorted();
83 // console.log(tmp.join(', '));
84
85
86 // we init counter with 0 value
87 for(i = 0; i < this.bounds.length; i++) {
88 this.counter[i]= 0;
89 }
90
91 for(j=0; j < tmp.length; j++) {
92
93 // get current class for value to increment the counter
94 var cclass = this.getClass(tmp[j]);
95 this.counter[cclass]++;
96
97 }
98
99 };
100
101 /**
102 * Transform a bounds array to a range array the following array : array(0,
103 * 0.75, 1.5, 2.25, 3); becomes : array('0-0.75', '0.75-1.5', '1.5-2.25',
104 * '2.25-3');
105 */
106 this.setRanges = function() {
107
108 this.ranges = Array(); // init empty array to prevent bug when calling classification after another with less items (sample getQuantile(6) and getQuantile(4))
109
110 for (i = 0; i < (this.bounds.length - 1); i++) {
111 this.ranges[i] = this.bounds[i] + this.separator + this.bounds[i + 1];
112 }
113 };
114
115 /** return min value */
116 this.min = function() {
117
118 if (this._nodata())
119 return;
120
121 if (this.stat_min == null) {
122
123 this.stat_min = this.serie[0];
124 for (i = 0; i < this.pop(); i++) {
125 if (this.serie[i] < this.stat_min) {
126 this.stat_min = this.serie[i];
127 }
128 }
129
130 }
131
132 return this.stat_min;
133 };
134
135 /** return max value */
136 this.max = function() {
137
138 if (this._nodata())
139 return;
140
141 if (this.stat_max == null) {
142
143 this.stat_max = this.serie[0];
144 for (i = 0; i < this.pop(); i++) {
145 if (this.serie[i] > this.stat_max) {
146 this.stat_max = this.serie[i];
147 }
148 }
149
150 }
151
152 return this.stat_max;
153 };
154
155 /** return sum value */
156 this.sum = function() {
157
158 if (this._nodata())
159 return;
160
161 if (this.stat_sum == null) {
162
163 this.stat_sum = 0;
164 for (i = 0; i < this.pop(); i++) {
165 this.stat_sum += this.serie[i];
166 }
167
168 }
169
170 return this.stat_sum;
171 };
172
173 /** return population number */
174 this.pop = function() {
175
176 if (this._nodata())
177 return;
178
179 if (this.stat_pop == null) {
180
181 this.stat_pop = this.serie.length;
182
183 }
184
185 return this.stat_pop;
186 };
187
188 /** return mean value */
189 this.mean = function() {
190
191 if (this._nodata())
192 return;
193
194 if (this.stat_mean == null) {
195
196 this.stat_mean = this.sum() / this.pop();
197
198 }
199
200 return this.stat_mean;
201 };
202
203 /** return median value */
204 this.median = function() {
205
206 if (this._nodata())
207 return;
208
209 if (this.stat_median == null) {
210
211 this.stat_median = 0;
212 var tmp = this.sorted();
213
214 if (tmp.length % 2) {
215 this.stat_median = tmp[(Math.ceil(tmp.length / 2) - 1)];
216 } else {
217 this.stat_median = (tmp[(tmp.length / 2) - 1] + tmp[(tmp.length / 2)]) / 2;
218 }
219
220 }
221
222 return this.stat_median;
223 };
224
225 /** return variance value */
226 this.variance = function() {
227
228 round = (typeof round === "undefined") ? true : false;
229
230 if (this._nodata())
231 return;
232
233 if (this.stat_variance == null) {
234
235 var tmp = 0;
236 for (var i = 0; i < this.pop(); i++) {
237 tmp += Math.pow( (this.serie[i] - this.mean()), 2 );
238 }
239
240 this.stat_variance = tmp / this.pop();
241
242 if(round == true) {
243 this.stat_variance = Math.round(this.stat_variance * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
244 }
245
246 }
247
248 return this.stat_variance;
249 };
250
251 /** return standard deviation value */
252 this.stddev = function(round) {
253
254 round = (typeof round === "undefined") ? true : false;
255
256 if (this._nodata())
257 return;
258
259 if (this.stat_stddev == null) {
260
261 this.stat_stddev = Math.sqrt(this.variance());
262
263 if(round == true) {
264 this.stat_stddev = Math.round(this.stat_stddev * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
265 }
266
267 }
268
269 return this.stat_stddev;
270 };
271
272 /** coefficient of variation - measure of dispersion */
273 this.cov = function(round) {
274
275 round = (typeof round === "undefined") ? true : false;
276
277 if (this._nodata())
278 return;
279
280 if (this.stat_cov == null) {
281
282 this.stat_cov = this.stddev() / this.mean();
283
284 if(round == true) {
285 this.stat_cov = Math.round(this.stat_cov * Math.pow(10,this.roundlength) )/ Math.pow(10,this.roundlength);
286 }
287
288 }
289
290 return this.stat_cov;
291 };
292
293 /** data test */
294 this._nodata = function() {
295 if (this.serie.length == 0) {
296
297 alert("Error. You should first enter a serie!");
298 return 1;
299 } else
300 return 0;
301
302 };
303
304 /** return sorted values (as array) */
305 this.sorted = function() {
306
307 if (this.stat_sorted == null) {
308
309 if(this.is_uniqueValues == false) {
310 this.stat_sorted = this.serie.sort(function(a, b) {
311 return a - b;
312 });
313 } else {
314 this.stat_sorted = this.serie.sort(function(a,b){
315 var nameA=a.toLowerCase(), nameB=b.toLowerCase()
316 if(nameA < nameB) return -1;
317 if(nameA > nameB) return 1;
318 return 0;
319 })
320 }
321 }
322
323 return this.stat_sorted;
324
325 };
326
327 /** return all info */
328 this.info = function() {
329
330 if (this._nodata())
331 return;
332
333 var content = '';
334 content += _t('Population') + ' : ' + this.pop() + ' - [' + _t('Min')
335 + ' : ' + this.min() + ' | ' + _t('Max') + ' : ' + this.max()
336 + ']' + "\n";
337 content += _t('Mean') + ' : ' + this.mean() + ' - ' + _t('Median') + ' : ' + this.median() + "\n";
338 content += _t('Variance') + ' : ' + this.variance() + ' - ' + _t('Standard deviation') + ' : ' + this.stddev()
339 + ' - ' + _t('Coefficient of variation') + ' : ' + this.cov() + "\n";
340
341 return content;
342 };
343
344 /**
345 * Equal intervals discretization Return an array with bounds : ie array(0,
346 * 0.75, 1.5, 2.25, 3);
347 */
348 this.getEqInterval = function(nbClass) {
349
350 if (this._nodata())
351 return;
352
353 this.method = _t('eq. intervals') + ' (' + nbClass + ' ' + _t('classes')
354 + ')';
355
356 var a = Array();
357 var val = this.min();
358 var interval = (this.max() - this.min()) / nbClass;
359
360 for (i = 0; i <= nbClass; i++) {
361 a[i] = val;
362 val += interval;
363 }
364
365 this.bounds = a;
366 this.setRanges();
367
368 return a;
369 };
370
371
372 /**
373 * Quantile discretization Return an array with bounds : ie array(0, 0.75,
374 * 1.5, 2.25, 3);
375 */
376 this.getQuantile = function(nbClass) {
377
378 if (this._nodata())
379 return;
380
381 this.method = _t('quantile') + ' (' + nbClass + ' ' + _t('classes') + ')';
382
383 var a = Array();
384 var tmp = this.sorted();
385
386 var classSize = Math.round(this.pop() / nbClass);
387 var step = classSize;
388 var i = 0;
389
390 // we set first value
391 a[0] = tmp[0];
392
393 for (i = 1; i < nbClass; i++) {
394 a[i] = tmp[step];
395 step += classSize;
396 }
397 // we set last value
398 a.push(tmp[tmp.length - 1]);
399
400 this.bounds = a;
401 this.setRanges();
402
403 return a;
404
405 };
406
407 /**
408 * Credits : Doug Curl (javascript) and Daniel J Lewis (python implementation)
409 * http://www.arcgis.com/home/item.html?id=0b633ff2f40d412995b8be377211c47b
410 * http://danieljlewis.org/2010/06/07/jenks-natural-breaks-algorithm-in-python/
411 */
412 this.getJenks = function(nbClass) {
413
414 if (this._nodata())
415 return;
416
417 this.method = _t('Jenks') + ' (' + nbClass + ' ' + _t('classes') + ')';
418
419 dataList = this.sorted();
420
421 // now iterate through the datalist:
422 // determine mat1 and mat2
423 // really not sure how these 2 different arrays are set - the code for
424 // each seems the same!
425 // but the effect are 2 different arrays: mat1 and mat2
426 var mat1 = []
427 for ( var x = 0, xl = dataList.length + 1; x < xl; x++) {
428 var temp = []
429 for ( var j = 0, jl = nbClass + 1; j < jl; j++) {
430 temp.push(0)
431 }
432 mat1.push(temp)
433 }
434
435 var mat2 = []
436 for ( var i = 0, il = dataList.length + 1; i < il; i++) {
437 var temp2 = []
438 for ( var c = 0, cl = nbClass + 1; c < cl; c++) {
439 temp2.push(0)
440 }
441 mat2.push(temp2)
442 }
443
444 // absolutely no idea what this does - best I can tell, it sets the 1st
445 // group in the
446 // mat1 and mat2 arrays to 1 and 0 respectively
447 for ( var y = 1, yl = nbClass + 1; y < yl; y++) {
448 mat1[0][y] = 1
449 mat2[0][y] = 0
450 for ( var t = 1, tl = dataList.length + 1; t < tl; t++) {
451 mat2[t][y] = Infinity
452 }
453 var v = 0.0
454 }
455
456 // and this part - I'm a little clueless on - but it works
457 // pretty sure it iterates across the entire dataset and compares each
458 // value to
459 // one another to and adjust the indices until you meet the rules:
460 // minimum deviation
461 // within a class and maximum separation between classes
462 for ( var l = 2, ll = dataList.length + 1; l < ll; l++) {
463 var s1 = 0.0
464 var s2 = 0.0
465 var w = 0.0
466 for ( var m = 1, ml = l + 1; m < ml; m++) {
467 var i3 = l - m + 1
468 var val = parseFloat(dataList[i3 - 1])
469 s2 += val * val
470 s1 += val
471 w += 1
472 v = s2 - (s1 * s1) / w
473 var i4 = i3 - 1
474 if (i4 != 0) {
475 for ( var p = 2, pl = nbClass + 1; p < pl; p++) {
476 if (mat2[l][p] >= (v + mat2[i4][p - 1])) {
477 mat1[l][p] = i3
478 mat2[l][p] = v + mat2[i4][p - 1]
479 }
480 }
481 }
482 }
483 mat1[l][1] = 1
484 mat2[l][1] = v
485 }
486
487 var k = dataList.length
488 var kclass = []
489
490 // fill the kclass (classification) array with zeros:
491 for (i = 0, il = nbClass + 1; i < il; i++) {
492 kclass.push(0)
493 }
494
495 // this is the last number in the array:
496 kclass[nbClass] = parseFloat(dataList[dataList.length - 1])
497 // this is the first number - can set to zero, but want to set to lowest
498 // to use for legend:
499 kclass[0] = parseFloat(dataList[0])
500 var countNum = nbClass
501 while (countNum >= 2) {
502 var id = parseInt((mat1[k][countNum]) - 2)
503 kclass[countNum - 1] = dataList[id]
504 k = parseInt((mat1[k][countNum] - 1))
505 // spits out the rank and value of the break values:
506 // console.log("id="+id,"rank = " + String(mat1[k][countNum]),"val =
507 // " + String(dataList[id]))
508 // count down:
509 countNum -= 1
510 }
511 // check to see if the 0 and 1 in the array are the same - if so, set 0
512 // to 0:
513 if (kclass[0] == kclass[1]) {
514 kclass[0] = 0
515 }
516
517 this.bounds = kclass;
518 this.setRanges();
519
520 return kclass; //array of breaks
521 }
522
523
524 /**
525 * Quantile discretization Return an array with bounds : ie array(0, 0.75,
526 * 1.5, 2.25, 3);
527 */
528 this.getUniqueValues = function() {
529
530 if (this._nodata())
531 return;
532
533 this.method = _t('unique values');
534 this.is_uniqueValues = true;
535
536 var tmp = this.sorted(); // display in alphabetical order
537
538 var a = Array();
539
540 for (i = 0; i < this.pop(); i++) {
541 if(!inArray (tmp[i], a))
542 a.push(tmp[i]);
543 }
544
545 this.bounds = a;
546
547 return a;
548
549 };
550
551
552 /**
553 * Return the class of a given value.
554 * For example value : 6
555 * and bounds array = (0, 4, 8, 12);
556 * Return 2
557 */
558 this.getClass = function(value) {
559
560 for(i = 0; i < this.bounds.length; i++) {
561
562
563 if(this.is_uniqueValues == true) {
564 if(value == this.bounds[i])
565 return i;
566 } else {
567 if(value <= this.bounds[i + 1]) {
568 return i;
569 }
570 }
571 }
572
573 return _t("Unable to get value's class.");
574
575 };
576
577 /**
578 * Return the ranges array : array('0-0.75', '0.75-1.5', '1.5-2.25',
579 * '2.25-3');
580 */
581 this.getRanges = function() {
582
583 return this.ranges;
584
585 };
586
587 /**
588 * Returns the number/index of this.ranges that value falls into
589 */
590 this.getRangeNum = function(value) {
591 var bounds, i;
592
593 for (i = 0; i < this.ranges.length; i++) {
594 bounds = this.ranges[i].split(/ - /);
595 if (value <= parseFloat(bounds[1])) {
596 return i;
597 }
598 }
599 }
600
601 /**
602 * Return an html legend
603 *
604 */
605 this.getHtmlLegend = function(colors, legend, counter, callback) {
606
607 var cnt= '';
608
609 if(colors != null) {
610 ccolors = colors;
611 }
612 else {
613 ccolors = this.colors;
614 }
615
616 if(legend != null) {
617 lg = legend;
618 }
619 else {
620 lg = 'Legend';
621 }
622
623 if(counter != null) {
624 this.doCount();
625 getcounter = true;
626 }
627 else {
628 getcounter = false;
629 }
630
631 if(callback != null) {
632 fn = callback;
633 }
634 else {
635 fn = function(o) {return o;};
636 }
637
638
639 if(ccolors.length < this.ranges.length) {
640 alert(_t('The number of colors should fit the number of ranges. Exit!'));
641 return;
642 }
643
644 var content = '<div class="geostats-legend"><div class="geostats-legend-title">' + _t(lg) + '</div>';
645
646 if(this.is_uniqueValues == false) {
647 for (i = 0; i < (this.ranges.length); i++) {
648 if(getcounter===true) {
649 cnt = ' <span class="geostats-legend-counter">(' + this.counter[i] + ')</span>';
650 }
651
652 // check if it has separator or not
653 if(this.ranges[i].indexOf(this.separator) != -1) {
654 var tmp = this.ranges[i].split(this.separator);
655 var el = fn(tmp[0]) + this.legendSeparator + fn(tmp[1]);
656 } else {
657 var el = fn(this.ranges[i]);
658 }
659 content += '<div><div class="geostats-legend-block" style="background-color:' + ccolors[i] + '"></div> ' + el + cnt + '</div>';
660 }
661 } else {
662 // only if classification is done on unique values
663 for (i = 0; i < (this.bounds.length); i++) {
664 if(getcounter===true) {
665 cnt = ' <span class="geostats-legend-counter">(' + this.counter[i] + ')</span>';
666 }
667 var el = fn(this.bounds[i]);
668 content += '<div><div class="geostats-legend-block" style="background-color:' + ccolors[i] + '"></div> ' + el + cnt + '</div>';
669 }
670 }
671 content += '</div>';
672 return content;
673 };
674
675 this.getSortedlist = function() {
676 return this.sorted().join(', ');
677 };
678
679};