Lately, I had a requirement building a "thematic mapping" application using openlayers 3 in order to visualise socio-economic data. OL3 does not provide any functionality related to statistical mapping. So googling it,
I fell into geostats .
A tiny standalone JS library for classification. It is like a hidden treasure, which I never had the chance to test it. Within the project page there are a few examples related to OL2 but nothing related to OL3. So I though it would be nice to share my experience and give a boost to all GIS geeks looking forward to use the library in conjunction with OL3.
It currently supports the following statistical methods
- equal intervals
- quantiles
- standard deviation (seems to have a small bug here)
- arithmetic progression
- geometric progression
- jenks (natural breaks)
- uniques values
- user defined classification
So lets go for an example. Lets build a thematic population map for world countries.
If you are too impatient ...... get a snapshot
But lets examine our code step by step:
Here is your html:
Nothing special. A selector for the method selected, a number spinner to get the number classes and two divs. One for the map and one to place the generated legend. And of course a button to draw our thematic map.
<select id="methodselector" class="form-control">
<option value="method_EI" >Equal Interval</option>
<option value="method_Q" selected>Quantile</option>
<option value="method_SD" >Standard Deviation</option>
<option value="method_AP" >Arithmetic Progression</option>
<option value="method_GP" >Geometric Progression</option>
<option value="method_CJ">Class Jenks</option>
</select>
Number of Classes:
<input type="number" id="classcount" min="1" value="5" max="10">
<input type="button" id="drawitbtn" value='draw themmatic'/>
<div id="map" class="map"></div>
<div id='legend'></div>
Dont forget to reference the necessary js, css libs within your html file. These are::
<link rel="stylesheet" type="text/css" href="https://openlayers.org/en/v3.19.1/css/ol.css" />
<script src="https://openlayers.org/en/v3.19.1/build/ol-debug.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.2.1/chroma.min.js"></script>
<script src="https://www.intermezzo-coop.eu/mapping/geostats/lib/geostats.js"></script>
:
Here is your css:
Again nothing special. Just a few classes to support the legend.
.geostats-legend div {
margin: 3px 10px 5px 10px
}
.geostats-legend-title {
font-weight: bold;
margin-bottom: 4px;
}
.geostats-legend-block {
border: 1px solid #555555;
display: block;
float: left;
height: 12px;
margin: 0 5px 0 20px;
width: 20px;
}
.geostats-legend-counter {
font-size: 0.8em;
color: #666;
font-style: italic;
}
And finally your JAVASCRIPT:
just go through the comments of code
$('#drawitbtn').click(drawIt);
//some global vars here
var classSeries;
var classColors;
//color start from
var colorFrom ='FFFFFF';
//color end to
var colorTo = '1A8B16';
var defaultStyle = new ol.style.Style({
fill: new ol.style.Fill({
color: 'rgba(255, 255, 255, 0.3)'
}),
stroke: new ol.style.Stroke({
color: 'rgba(0, 255, 0, 1)',
width: 1
}),
text: new ol.style.Text({
font: '12px Calibri,sans-serif',
fill: new ol.style.Fill({
color: '#000'
}),
stroke: new ol.style.Stroke({
color: '#fff',
width: 3
})
})
});
//our methods here
var vectorLayer = new ol.layer.Vector({
style:defaultStyle,
source: new ol.source.Vector({
url: 'https://gist.githubusercontent.com/ptsagkis/dacbe5a42856dee041294b54579095d4/raw/730879d0d4909622b273818235b9c7f510bab5ee/countries_siplified.geojson',
format: new ol.format.GeoJSON({
defaultDataProjection:'EPSG:4326',
featureProjection:'EPSG:3857'
})
})
});
var map = new ol.Map({
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
}),
vectorLayer
],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 1
})
});
/**
* do the themmatic
*/
function drawIt(){
var countryPopVals = new Array();
vectorLayer.getSource().getFeatures().forEach(function(feat) {
countryPopVals.push(feat.get("POP2005"))
});
console.info("countryPopVals",countryPopVals);
getAndSetClassesFromData(countryPopVals, getClassNum(), getMethod());
vectorLayer.setStyle(setStyle);
}
/**
* @data {Array} the array of numbers (these are the pop data for all countries)
* @numclasses {Integer} get the number of classes
* @method {String} get the classification method
*
*
* set geostats object
* set the series
* set the colors ramp
*
*/
function getAndSetClassesFromData(data, numclasses, method) {
var serie = new geostats(data);
var legenLabel = "";
if (method === "method_EI") {
serie.getClassEqInterval(numclasses);
methodLabel = "Equal Interval";
} else if (method === "method_Q") {
serie.getClassQuantile(numclasses);
methodLabel = "Quantile";
} else if (method === "method_SD") {
serie.getClassStdDeviation(numclasses);
methodLabel = "Standard Deviation ";
} else if (method === "method_AP") {
serie.getClassArithmeticProgression(numclasses);
methodLabel = "Arithmetic Progression";
} else if (method === "method_GP") {
serie.getClassGeometricProgression(numclasses);
methodLabel = "Geometric Progression ";
} else if (method === "method_CJ") {
serie.getClassJenks(numclasses);
methodLabel = "Class Jenks";
} else {
alert("error: no such method.")
}
var colors_x = chroma.scale([colorFrom, colorTo]).colors(numclasses)
serie.setColors(colors_x);
document.getElementById('legend').innerHTML = serie.getHtmlLegend(null, "World Population</br> Method:"+methodLabel, 1);
classSeries = serie;
classColors = colors_x;
}
/**
* function to verify the style for the feature
*/
function setStyle(feat,res) {
var currVal = parseFloat(feat.get("POP2005"));
var bounds = classSeries.bounds;
var numRanges = new Array();
for (var i = 0; i < bounds.length-1; i++) {
numRanges.push({
min: parseFloat(bounds[i]),
max: parseFloat(bounds[i+1])
});
}
var classIndex = verifyClassFromVal(numRanges, currVal);
var polyStyleConfig = {
stroke: new ol.style.Stroke({
color: 'rgba(255, 0, 0,0.3)',
width: 1
})
};
var textStyleConfig = {};
var label = res < 5000 ? feat.get('NAME') : '';
if (classIndex !== -1) {
polyStyleConfig = {
stroke: new ol.style.Stroke({
color: 'rgba(0, 0, 255, 1.0)',
width: 1
}),
fill: new ol.style.Stroke({
color: hexToRgbA(classColors[classIndex],0.7)
})
};
textStyleConfig = {
text: new ol.style.Text({
text: label,
font: '12px Calibri,sans-serif',
fill: new ol.style.Fill({
color: "#000000"
}),
stroke: new ol.style.Stroke({
color: "#FFFFFF",
width: 2
})
}),
geometry: function(feature) {
var retPoint;
if (feature.getGeometry().getType() === 'MultiPolygon') {
retPoint = getMaxPoly(feature.getGeometry().getPolygons()).getInteriorPoint();
} else if (feature.getGeometry().getType() === 'Polygon') {
retPoint = feature.getGeometry().getInteriorPoint();
}
return retPoint;
}
}
};
var textStyle = new ol.style.Style(textStyleConfig);
var style = new ol.style.Style(polyStyleConfig);
return [style, textStyle];
}
//*************helper functions this point forward***************//
function verifyClassFromVal(rangevals, val) {
var retIndex = -1;
for (var i = 0; i < rangevals.length; i++) {
if (val >= rangevals[i].min && val <= rangevals[i].max) {
retIndex = i;
}
}
return retIndex;
}
/**
* get the user selected method
*/
function getMethod(){
var elem = document.getElementById("methodselector");
var val = elem.options[elem.selectedIndex].value;
return val;
}
/**
* get the user selected number of classes
*/
function getClassNum(){
var elem = document.getElementById("classcount");
return parseInt(elem.value);
}
/**
* convert hex to rgba
*
*/
function hexToRgbA(hex,opacity) {
var c;
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
c = hex.substring(1).split('');
if (c.length == 3) {
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
}
c = '0x' + c.join('');
return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ','+opacity+')';
}
throw new Error('Bad Hex');
}
/**
* get the maximum polygon out of the supllied array of polygon
* used for labeling the bigger one
*/
function getMaxPoly(polys) {
var polyObj = [];
//now need to find which one is the greater and so label only this
for (var b = 0; b < polys.length; b++) {
polyObj.push({
poly: polys[b],
area: polys[b].getArea()
});
}
polyObj.sort(function(a, b) {
return a.area - b.area
});
return polyObj[polyObj.length - 1].poly;
}
Enjoy all of you