Skip to content

Commit 76caf01

Browse files
committed
- new classification mode: standard deviation (mode: stddeviation)
-- class intervals are defined using standard deviation from the mean of the dataset -- results in equal class widths and varying amount of features per class -- as always, it is intended for use with normally distributed data -- creates classes with an interval size of 1 standard deviation (support for 1/2, 1/3 std. dev. coming soon) -- with this mode, option `classes` is ignored -- legend customization recommended (by making the unit of values clear, e.g. including unit "std. dev." in legend title, or by defining custom templates for legend rows to show unit) - fixed and cleaned up point/color mode symbol radius defaults - updated documentation - examples: -- `lines_w.html`: uses new data of river discharge, improved mouse hover tooltips and added feature highlighting -- updated dataset `rivers.geojson`: contains river discharge (flow) data and river names now
1 parent 33d7da9 commit 76caf01

File tree

4 files changed

+296
-43
lines changed

4 files changed

+296
-43
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Aims to simplify data visualization and creation of elegant thematic web maps wi
1515
- natural breaks (Jenks)
1616
- quantile (equal count)
1717
- equal interval
18+
- standard deviation
1819
- manual
1920
- Supports ColorBrewer2 color ramps and custom color ramps (thanks to [chroma.js](https://github.com/gka/chroma.js))
2021
- Various SVG shapes/symbols for Point features
@@ -102,7 +103,7 @@ const layer = L.dataClassification(data, {
102103
```
103104
104105
### Required options
105-
- `mode <string>`: ['jenks'|'quantile'|'equalinterval'|'manual'] classification method: jenks, quantile, equalinterval, manual. When using manual (which partially defeats the purpose of this plugin), option `classes` must be an array of class boundary values!
106+
- `mode <string>`: ['jenks'|'quantile'|'equalinterval'|'stddeviation'|'manual'] classification method: natural break (Jenks), equal count (quantile), equal interval, standard deviation, manual. When using standard deviation, option `classes` is ignored. When using manual (which partially defeats the purpose of this plugin), option `classes` must be an array of class boundary values!
106107
- `classes <integer|array>`: desired number of classes (min: 3; max: 10 or featurecount, whichever is lower. If higher, reverts back to the max of 10.). If `mode` is manual, this must be an array of numbers (for example [0, 150, 200] would yield the following three classes: below 150, 150-200, above 200).
107108
- `field <string>`: target attribute field name to base classification on. Case-sensitive!
108109

examples/data/rivers.geojson

+110-14
Large diffs are not rendered by default.

examples/lines_w.html

+35-11
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,49 @@
7979

8080
// Line features example. Attribute to test with: 'Shape_Leng'
8181
fetch('data/rivers.geojson').then(r => r.json()).then(d => {
82-
function tooltip(feature, layer) {
83-
if (feature.properties.Shape_Leng) {
84-
layer.bindTooltip(String(feature.properties.Shape_Leng));
82+
83+
var origstyle;
84+
85+
function highlight(e) {
86+
origstyle = {
87+
weight: e.target.options.weight,
88+
color: e.target.options.color
89+
}
90+
e.target.setStyle({
91+
color: '#f22',
92+
});
93+
}
94+
95+
function resetStyle(e) {
96+
e.target.setStyle(origstyle);
97+
}
98+
99+
function oEF(feature, layer) {
100+
if (feature.properties.discharge) {
101+
layer.bindTooltip('<b>' + String(feature.properties.NAME) + '</b><br>' + String(feature.properties.discharge) + ' m³/s');
102+
} else {
103+
layer.bindTooltip('<b>' + String(feature.properties.NAME) + '</b><br>' + 'no data');
85104
}
105+
layer.on({
106+
mouseover: highlight,
107+
mouseout: resetStyle
108+
});
86109
}
110+
87111
window.testdata = L.dataClassification(d, {
88112
mode: 'jenks',
89-
classes: 3,
90-
field: 'Shape_Leng',
113+
classes: 4,
114+
field: 'discharge',
91115
lineMode: 'width',
92116
lineWidth: {
93117
min: 2,
94-
max: 10
118+
max: 12
95119
},
96-
classRounding: -5,
97-
unitModifier: {action: 'divide', by: 1000},
120+
//classRounding: -2,
121+
//unitModifier: {action: 'divide', by: 1000},
98122
legendAscending: false,
99-
legendTitle: 'Length (km)',
100-
onEachFeature: tooltip
123+
legendTitle: 'Discharge (m³/s)',
124+
onEachFeature: oEF
101125
}).addTo(map);
102126
map.fitBounds(testdata.getBounds());
103127
});
@@ -112,7 +136,7 @@
112136
'</div>'+
113137
'<div style="justify-content: center; ">' +
114138
'This is an example page showcasing some of the features of Leaflet plugin <i>leaflet-dataclassification</i>. '+
115-
'Feature tooltips on hover (native feature of Leaflet) were added to provide an easy check of attribute values used. '+
139+
'Feature tooltips on hover and feature highlighting (native features of Leaflet) were added to provide an easy check of attribute values used. '+
116140
'<br><br>'+
117141
'Single-step data classification, symbology and legend creation for GeoJSON data powered thematic maps.'+
118142
'<br><br>'+

leaflet-dataclassification.js

+149-17
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ L.DataClassification = L.GeoJSON.extend({
1515
options: {
1616
// NOTE: documentation in this object might not be up to date. Please always refer to the documentation on GitHub.
1717
// default options
18-
mode: 'quantile', // classification method: jenks, quantile, equalinterval, manual (when using manual, `classes` must be an array!)
18+
mode: 'quantile', // classification method: jenks, quantile, equalinterval, stddeviation (when using stddev, `classes` is ignored!), manual (when using manual, `classes` must be an array!)
1919
classes: 5, // desired number of classes (min: 3, max: 10 or featurecount, whichever is lower)
2020
pointMode: 'color', // POINT FEATURES: fill "color" or "size" (default: color)
2121
pointSize: {min: 2, max: 10}, // POINT FEATURES: when pointMode: "size", define min/max point circle radius (default min: 2, default max: 10, recommended max: 12)
@@ -62,7 +62,7 @@ L.DataClassification = L.GeoJSON.extend({
6262
fillOpacity: 0.7,
6363
color: L.Path.prototype.options.color,
6464
weight: L.Path.prototype.options.weight,
65-
raidus: 8
65+
radius: 8
6666
}
6767
},
6868

@@ -175,7 +175,7 @@ L.DataClassification = L.GeoJSON.extend({
175175
color: "black",
176176
weight: 1,
177177
shape: "circle",
178-
radius: (options.style.radius != null ? options.style.radius : 8)
178+
radius: options.style.radius
179179
};
180180
},
181181

@@ -576,8 +576,17 @@ L.DataClassification = L.GeoJSON.extend({
576576
// color based categories
577577
for (var i = classes.length; i > 0; i--) {
578578
/*console.debug('Legend: building line', i)*/
579-
let low = classes[i-1].value;
580-
let high = (classes[i] != null ? classes[i].value : '');
579+
let low, high;
580+
switch (mode) {
581+
case 'stddeviation':
582+
low = classes[i-1].stddev_lower;
583+
high = (classes[i] != null ? classes[i].stddev_lower : '');
584+
break;
585+
default:
586+
low = classes[i-1].value;
587+
high = (classes[i] != null ? classes[i].value : '');
588+
break;
589+
}
581590
container.innerHTML +=
582591
'<div class="legendDataRow">'+
583592
svgCreator({shape: ps, color: colors[i-1], size: prad})+
@@ -595,9 +604,18 @@ L.DataClassification = L.GeoJSON.extend({
595604
case 'size':
596605
// size (radius) based categories
597606
for (var i = classes.length; i > 0; i--) {
598-
// decide low and high boundary values for current legend row (class)
599-
let low = classes[i-1].value;
600-
let high = (classes[i] != null ? classes[i].value : '');
607+
let low, high;
608+
switch (mode) {
609+
case 'stddeviation':
610+
low = classes[i-1].stddev_lower;
611+
high = (classes[i] != null ? classes[i].stddev_lower : '');
612+
break;
613+
default:
614+
// decide low and high boundary values for current legend row (class)
615+
low = classes[i-1].value;
616+
high = (classes[i] != null ? classes[i].value : '');
617+
break;
618+
}
601619

602620
// generate row with symbol
603621
container.innerHTML +=
@@ -621,9 +639,19 @@ L.DataClassification = L.GeoJSON.extend({
621639
case 'color':
622640
// color based categories
623641
for (var i = classes.length; i > 0; i--) {
624-
/*console.debug('Legend: building line', i)*/
625-
let low = classes[i-1].value;
626-
let high = (classes[i] != null ? classes[i].value : '');
642+
let low, high;
643+
switch (mode) {
644+
case 'stddeviation':
645+
low = classes[i-1].stddev_lower;
646+
high = (classes[i] != null ? classes[i].stddev_lower : '');
647+
break;
648+
default:
649+
/*console.debug('Legend: building line', i)*/
650+
low = classes[i-1].value;
651+
high = (classes[i] != null ? classes[i].value : '');
652+
break;
653+
}
654+
627655
container.innerHTML +=
628656
'<div class="legendDataRow">'+
629657
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px;">'+
@@ -646,8 +674,17 @@ L.DataClassification = L.GeoJSON.extend({
646674
// width based categories
647675
for (var i = classes.length; i > 0; i--) {
648676
/*console.debug('Legend: building line', i)*/
649-
let low = classes[i-1].value;
650-
let high = (classes[i] != null ? classes[i].value : '');
677+
let low, high;
678+
switch (mode) {
679+
case 'stddeviation':
680+
low = classes[i-1].stddev_lower;
681+
high = (classes[i] != null ? classes[i].stddev_lower : '');
682+
break;
683+
default:
684+
low = classes[i-1].value;
685+
high = (classes[i] != null ? classes[i].value : '');
686+
break;
687+
}
651688
container.innerHTML +=
652689
'<div class="legendDataRow">'+
653690
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px;">'+
@@ -674,8 +711,17 @@ L.DataClassification = L.GeoJSON.extend({
674711
case 'color':
675712
for (var i = classes.length; i > 0; i--) {
676713
/*console.debug('Legend: building line', i)*/
677-
let low = classes[i-1].value;
678-
let high = (classes[i] != null ? classes[i].value : '');
714+
let low, high;
715+
switch (mode) {
716+
case 'stddeviation':
717+
low = classes[i-1].stddev_lower;
718+
high = (classes[i] != null ? classes[i].stddev_lower : '');
719+
break;
720+
default:
721+
low = classes[i-1].value;
722+
high = (classes[i] != null ? classes[i].value : '');
723+
break;
724+
}
679725
container.innerHTML +=
680726
'<div class="legendDataRow">'+
681727
'<i style="background: ' + colors[i-1] + '; opacity: ' + opacity + '"></i>' +
@@ -693,8 +739,17 @@ L.DataClassification = L.GeoJSON.extend({
693739
case 'hatch':
694740
for (var i = classes.length; i > 0; i--) {
695741
/*console.debug('Legend: building line', i)*/
696-
let low = classes[i-1].value;
697-
let high = (classes[i] != null ? classes[i].value : '');
742+
let low, high;
743+
switch (mode) {
744+
case 'stddeviation':
745+
low = classes[i-1].stddev_lower;
746+
high = (classes[i] != null ? classes[i].stddev_lower : '');
747+
break;
748+
default:
749+
low = classes[i-1].value;
750+
high = (classes[i] != null ? classes[i].value : '');
751+
break;
752+
}
698753
container.innerHTML +=
699754
'<div class="legendDataRow">'+
700755
'<svg class="hatchPatch"><rect fill="url(#'+hatchclasses[i-1]+')" fill-opacity="' + opacity + '" x="0" y="0" width="100%" height="100%"></rect></svg>'+
@@ -924,6 +979,83 @@ L.DataClassification = L.GeoJSON.extend({
924979
this._convertClassesToObjects();
925980
success = true;
926981
break;
982+
case 'stddeviation':
983+
// with zScore: (number-average)/standard_deviation
984+
classes = [];
985+
var stddev = ss.standardDeviation(values.filter((value) => value != null))
986+
var mean = ss.mean(values.filter((value) => value != null))
987+
console.debug('stddev:', stddev)
988+
console.debug('mean:', mean)
989+
var extent = ss.extent(values.filter((value) => value != null))
990+
/*console.debug('extent', extent)
991+
var diff = extent[1]-extent[0]
992+
console.debug('diff', diff)
993+
console.debug('number of classes if 1 stddev:', diff/(stddev/1))
994+
console.debug('number of classes if 1 stddev:', Math.round(diff/(stddev/1)))
995+
console.debug('number of classes if 1/2 stddev:', diff/(stddev/2))
996+
console.debug('number of classes if 1/3 stddev:', diff/(stddev/3))*/
997+
998+
var halfstddev = stddev/2;
999+
var curr;
1000+
var down = 1;
1001+
var up = 1;
1002+
//var potclassnum = Math.round(diff/(stddev/1));
1003+
var valid = true;
1004+
classes.push(-999999);
1005+
for (var i = 0; valid /*i<potclassnum*/; i++) {
1006+
console.debug('downwards', down)
1007+
curr = mean-(halfstddev*down);
1008+
console.debug('downwards curr', curr)
1009+
console.debug(extent[0])
1010+
console.debug((curr > extent[0]))
1011+
console.debug(halfstddev)
1012+
if (curr > extent[0] && down < 7) {
1013+
classes.push(curr);
1014+
down += 2;
1015+
} else {
1016+
valid = false;
1017+
};
1018+
}
1019+
1020+
valid = true;
1021+
for (var i = 0; valid /*i<potclassnum*/; i++) {
1022+
console.debug('upwards', up)
1023+
curr = mean+(halfstddev*up);
1024+
console.debug('upwards curr', curr)
1025+
if (curr < extent[1] && up < 7) {
1026+
classes.push(curr);
1027+
up += 2;
1028+
} else {
1029+
valid = false
1030+
};
1031+
}
1032+
1033+
console.debug(classes);
1034+
classes.sort(function(a, b) {
1035+
return a - b;
1036+
});
1037+
console.debug('Sorted Stddev classes: ', classes);
1038+
this._convertClassesToObjects();
1039+
1040+
console.debug('down:', down, 'up:', up)
1041+
console.debug('down intervals:', down, 'up intervals:', up)
1042+
var interval_lower = (0.5 * -down);
1043+
classes.forEach(function (arrayItem) {
1044+
if (down > 0) {
1045+
console.debug('down =', down, 'up =', up, 'boundary =', interval_lower)
1046+
arrayItem.stddev_lower = interval_lower;
1047+
interval_lower += 1;
1048+
down -= 2;
1049+
} else if (down < 0 && up > 0) {
1050+
console.debug('down =', down, 'up =', up, 'boundary =', interval_lower)
1051+
arrayItem.stddev_lower = interval_lower;
1052+
interval_lower += 1;
1053+
up -= 2;
1054+
1055+
};
1056+
});
1057+
success = true;
1058+
break;
9271059
// EXPERIMENTAL LOG
9281060
case 'logarithmic':
9291061
classes = [];

0 commit comments

Comments
 (0)