Skip to content

Commit

Permalink
- new classification mode: standard deviation (mode: stddeviation)
Browse files Browse the repository at this point in the history
-- 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
  • Loading branch information
balladaniel committed Dec 10, 2023
1 parent 33d7da9 commit 76caf01
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 43 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Aims to simplify data visualization and creation of elegant thematic web maps wi
- natural breaks (Jenks)
- quantile (equal count)
- equal interval
- standard deviation
- manual
- Supports ColorBrewer2 color ramps and custom color ramps (thanks to [chroma.js](https://github.com/gka/chroma.js))
- Various SVG shapes/symbols for Point features
Expand Down Expand Up @@ -102,7 +103,7 @@ const layer = L.dataClassification(data, {
```
### Required options
- `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!
- `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!
- `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).
- `field <string>`: target attribute field name to base classification on. Case-sensitive!
Expand Down
124 changes: 110 additions & 14 deletions examples/data/rivers.geojson

Large diffs are not rendered by default.

46 changes: 35 additions & 11 deletions examples/lines_w.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,49 @@

// Line features example. Attribute to test with: 'Shape_Leng'
fetch('data/rivers.geojson').then(r => r.json()).then(d => {
function tooltip(feature, layer) {
if (feature.properties.Shape_Leng) {
layer.bindTooltip(String(feature.properties.Shape_Leng));

var origstyle;

function highlight(e) {
origstyle = {
weight: e.target.options.weight,
color: e.target.options.color
}
e.target.setStyle({
color: '#f22',
});
}

function resetStyle(e) {
e.target.setStyle(origstyle);
}

function oEF(feature, layer) {
if (feature.properties.discharge) {
layer.bindTooltip('<b>' + String(feature.properties.NAME) + '</b><br>' + String(feature.properties.discharge) + ' m³/s');
} else {
layer.bindTooltip('<b>' + String(feature.properties.NAME) + '</b><br>' + 'no data');
}
layer.on({
mouseover: highlight,
mouseout: resetStyle
});
}

window.testdata = L.dataClassification(d, {
mode: 'jenks',
classes: 3,
field: 'Shape_Leng',
classes: 4,
field: 'discharge',
lineMode: 'width',
lineWidth: {
min: 2,
max: 10
max: 12
},
classRounding: -5,
unitModifier: {action: 'divide', by: 1000},
//classRounding: -2,
//unitModifier: {action: 'divide', by: 1000},
legendAscending: false,
legendTitle: 'Length (km)',
onEachFeature: tooltip
legendTitle: 'Discharge (m³/s)',
onEachFeature: oEF
}).addTo(map);
map.fitBounds(testdata.getBounds());
});
Expand All @@ -112,7 +136,7 @@
'</div>'+
'<div style="justify-content: center; ">' +
'This is an example page showcasing some of the features of Leaflet plugin <i>leaflet-dataclassification</i>. '+
'Feature tooltips on hover (native feature of Leaflet) were added to provide an easy check of attribute values used. '+
'Feature tooltips on hover and feature highlighting (native features of Leaflet) were added to provide an easy check of attribute values used. '+
'<br><br>'+
'Single-step data classification, symbology and legend creation for GeoJSON data powered thematic maps.'+
'<br><br>'+
Expand Down
166 changes: 149 additions & 17 deletions leaflet-dataclassification.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ L.DataClassification = L.GeoJSON.extend({
options: {
// NOTE: documentation in this object might not be up to date. Please always refer to the documentation on GitHub.
// default options
mode: 'quantile', // classification method: jenks, quantile, equalinterval, manual (when using manual, `classes` must be an array!)
mode: 'quantile', // classification method: jenks, quantile, equalinterval, stddeviation (when using stddev, `classes` is ignored!), manual (when using manual, `classes` must be an array!)
classes: 5, // desired number of classes (min: 3, max: 10 or featurecount, whichever is lower)
pointMode: 'color', // POINT FEATURES: fill "color" or "size" (default: color)
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)
Expand Down Expand Up @@ -62,7 +62,7 @@ L.DataClassification = L.GeoJSON.extend({
fillOpacity: 0.7,
color: L.Path.prototype.options.color,
weight: L.Path.prototype.options.weight,
raidus: 8
radius: 8
}
},

Expand Down Expand Up @@ -175,7 +175,7 @@ L.DataClassification = L.GeoJSON.extend({
color: "black",
weight: 1,
shape: "circle",
radius: (options.style.radius != null ? options.style.radius : 8)
radius: options.style.radius
};
},

Expand Down Expand Up @@ -576,8 +576,17 @@ L.DataClassification = L.GeoJSON.extend({
// color based categories
for (var i = classes.length; i > 0; i--) {
/*console.debug('Legend: building line', i)*/
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}
container.innerHTML +=
'<div class="legendDataRow">'+
svgCreator({shape: ps, color: colors[i-1], size: prad})+
Expand All @@ -595,9 +604,18 @@ L.DataClassification = L.GeoJSON.extend({
case 'size':
// size (radius) based categories
for (var i = classes.length; i > 0; i--) {
// decide low and high boundary values for current legend row (class)
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
// decide low and high boundary values for current legend row (class)
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}

// generate row with symbol
container.innerHTML +=
Expand All @@ -621,9 +639,19 @@ L.DataClassification = L.GeoJSON.extend({
case 'color':
// color based categories
for (var i = classes.length; i > 0; i--) {
/*console.debug('Legend: building line', i)*/
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
/*console.debug('Legend: building line', i)*/
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}

container.innerHTML +=
'<div class="legendDataRow">'+
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px;">'+
Expand All @@ -646,8 +674,17 @@ L.DataClassification = L.GeoJSON.extend({
// width based categories
for (var i = classes.length; i > 0; i--) {
/*console.debug('Legend: building line', i)*/
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}
container.innerHTML +=
'<div class="legendDataRow">'+
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px;">'+
Expand All @@ -674,8 +711,17 @@ L.DataClassification = L.GeoJSON.extend({
case 'color':
for (var i = classes.length; i > 0; i--) {
/*console.debug('Legend: building line', i)*/
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}
container.innerHTML +=
'<div class="legendDataRow">'+
'<i style="background: ' + colors[i-1] + '; opacity: ' + opacity + '"></i>' +
Expand All @@ -693,8 +739,17 @@ L.DataClassification = L.GeoJSON.extend({
case 'hatch':
for (var i = classes.length; i > 0; i--) {
/*console.debug('Legend: building line', i)*/
let low = classes[i-1].value;
let high = (classes[i] != null ? classes[i].value : '');
let low, high;
switch (mode) {
case 'stddeviation':
low = classes[i-1].stddev_lower;
high = (classes[i] != null ? classes[i].stddev_lower : '');
break;
default:
low = classes[i-1].value;
high = (classes[i] != null ? classes[i].value : '');
break;
}
container.innerHTML +=
'<div class="legendDataRow">'+
'<svg class="hatchPatch"><rect fill="url(#'+hatchclasses[i-1]+')" fill-opacity="' + opacity + '" x="0" y="0" width="100%" height="100%"></rect></svg>'+
Expand Down Expand Up @@ -924,6 +979,83 @@ L.DataClassification = L.GeoJSON.extend({
this._convertClassesToObjects();
success = true;
break;
case 'stddeviation':
// with zScore: (number-average)/standard_deviation
classes = [];
var stddev = ss.standardDeviation(values.filter((value) => value != null))
var mean = ss.mean(values.filter((value) => value != null))
console.debug('stddev:', stddev)
console.debug('mean:', mean)
var extent = ss.extent(values.filter((value) => value != null))
/*console.debug('extent', extent)
var diff = extent[1]-extent[0]
console.debug('diff', diff)
console.debug('number of classes if 1 stddev:', diff/(stddev/1))
console.debug('number of classes if 1 stddev:', Math.round(diff/(stddev/1)))
console.debug('number of classes if 1/2 stddev:', diff/(stddev/2))
console.debug('number of classes if 1/3 stddev:', diff/(stddev/3))*/

var halfstddev = stddev/2;
var curr;
var down = 1;
var up = 1;
//var potclassnum = Math.round(diff/(stddev/1));
var valid = true;
classes.push(-999999);
for (var i = 0; valid /*i<potclassnum*/; i++) {
console.debug('downwards', down)
curr = mean-(halfstddev*down);
console.debug('downwards curr', curr)
console.debug(extent[0])
console.debug((curr > extent[0]))
console.debug(halfstddev)
if (curr > extent[0] && down < 7) {
classes.push(curr);
down += 2;
} else {
valid = false;
};
}

valid = true;
for (var i = 0; valid /*i<potclassnum*/; i++) {
console.debug('upwards', up)
curr = mean+(halfstddev*up);
console.debug('upwards curr', curr)
if (curr < extent[1] && up < 7) {
classes.push(curr);
up += 2;
} else {
valid = false
};
}

console.debug(classes);
classes.sort(function(a, b) {
return a - b;
});
console.debug('Sorted Stddev classes: ', classes);
this._convertClassesToObjects();

console.debug('down:', down, 'up:', up)
console.debug('down intervals:', down, 'up intervals:', up)
var interval_lower = (0.5 * -down);
classes.forEach(function (arrayItem) {
if (down > 0) {
console.debug('down =', down, 'up =', up, 'boundary =', interval_lower)
arrayItem.stddev_lower = interval_lower;
interval_lower += 1;
down -= 2;
} else if (down < 0 && up > 0) {
console.debug('down =', down, 'up =', up, 'boundary =', interval_lower)
arrayItem.stddev_lower = interval_lower;
interval_lower += 1;
up -= 2;

};
});
success = true;
break;
// EXPERIMENTAL LOG
case 'logarithmic':
classes = [];
Expand Down

0 comments on commit 76caf01

Please sign in to comment.