in this iteration, we use the ckmeans algorithm from the simple-statistics package to cluster our data. we pick the minimum value of each cluster as a break. we then use these breaks with a quantile scale to map values in the data to colors on the choropleth map.
this is method is my current favorite way to create breaks, or color thresholds, for a choropleth map.
🎩 @Elijah_Meeks for the idea to try the ckmeans algorithm, as it seems to be the new hotness in the choropleth map breaks scene
a further 🙏 to @recifs for talking through where in the ckmeans clusters its reasonable to pick breaks from.
tl;dr any number between max(class n) and min(class n+1) is OK
do check out the other examples in this world map
series:
world map 00 original example
world map 01 fix tooltip value
world map 02 d3 v4
world map 03 es2015 + update code style
world map 04 manual breaks + threshold scale
world map 05 linear breaks + quantize scale
world map 06 linear breaks + quantiles scale
world map 07 Jenks natural breaks
world map 08 ckmeans cluster max breaks
world map 09 ckmeans cluster min breaks
forked from micahstubbs's block: world map 09 ckmeans cluster min breaks
forked from heillygalvez's block: world map 09 ckmeans cluster min breaks
forked from heillygalvez's block: world map 09 ckmeans cluster min breaks
xxxxxxxxxx
<meta charset="utf-8">
<style>
.names {
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
/* Tooltip CSS */
.d3-tip {
line-height: 1.5;
font-weight: 400;
font-family:"avenir next", Arial, sans-serif;
padding: 6px;
background: rgba(0, 0, 0, 0.6);
color: #FFA500;
border-radius: 1px;
pointer-events: none;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 8px;
width: 100%;
line-height: 1.5;
color: rgba(0, 0, 0, 0.6);
position: absolute;
pointer-events: none;
}
/* Northward tooltips */
.d3-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}
/* Eastward tooltips */
.d3-tip.e:after {
content: "\25C0";
margin: -4px 0 0 0;
top: 50%;
left: -8px;
}
/* Southward tooltips */
.d3-tip.s:after {
content: "\25B2";
margin: 0 0 1px 0;
top: -8px;
left: 0;
text-align: center;
}
/* Westward tooltips */
.d3-tip.w:after {
content: "\25B6";
margin: -4px 0 0 -1px;
top: 50%;
left: 100%;
}
/*
text{
pointer-events:none;
}
*/
.details{
color: white;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v1.min.js"></script>
<script src="d3-tip.js"></script>
<script src='https://unpkg.com/simple-statistics@2.0.0/dist/simple-statistics.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js'></script>
<script lang='babel' type='text/babel'>
// configuration
const colorVariable = 'population';
const geoIDVariable = 'id';
const format = d3.format(',');
// Set tooltips
const tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(d => `<strong>Country: </strong><span class='details'>${d.properties.name}<br></span><strong>Concentration: </strong><span class='details'>${format(d[colorVariable])}</span>`);
tip.direction(function(d) {
if (d.properties.name === 'Antarctica') return 'n';
// Americas
if (d.properties.name === 'Greenland') return 's';
if (d.properties.name === 'Canada') return 'e';
if (d.properties.name === 'USA') return 'e';
if (d.properties.name === 'Mexico') return 'e';
// Europe
if (d.properties.name === 'Iceland') return 's';
if (d.properties.name === 'Norway') return 's';
if (d.properties.name === 'Sweden') return 's';
if (d.properties.name === 'Finland') return 's';
if (d.properties.name === 'Russia') return 'w';
// Asia
if (d.properties.name === 'China') return 'w';
if (d.properties.name === 'Japan') return 's';
// Oceania
if (d.properties.name === 'Indonesia') return 'w';
if (d.properties.name === 'Papua New Guinea') return 'w';
if (d.properties.name === 'Australia') return 'w';
if (d.properties.name === 'New Zealand') return 'w';
// otherwise if not specified
return 'n';
})
tip.offset(function(d) { // [top, left]
if (d.properties.name === 'Antarctica') return [0, 0];
// Americas
if (d.properties.name === 'Greenland') return [10, -10];
if (d.properties.name === 'Canada') return [24, -28];
if (d.properties.name === 'USA') return [-5, 8];
if (d.properties.name === 'Mexico') return [12, 10];
if (d.properties.name === 'Chile') return [0, -15];
// Europe
if (d.properties.name === 'Iceland') return [15, 0];
if (d.properties.name === 'Norway') return [10, -28];
if (d.properties.name === 'Sweden') return [10, -8];
if (d.properties.name === 'Finland') return [10, 0];
if (d.properties.name === 'France') return [-9, 66];
if (d.properties.name === 'Italy') return [-8, -6];
if (d.properties.name === 'Russia') return [5, 385];
// Africa
if (d.properties.name === 'Madagascar') return [-10, 10];
// Asia
if (d.properties.name === 'China') return [-16, -8];
if (d.properties.name === 'Mongolia') return [-5, 0];
if (d.properties.name === 'Pakistan') return [-10, 13];
if (d.properties.name === 'India') return [-11, -18];
if (d.properties.name === 'Nepal') return [-8, 1];
if (d.properties.name === 'Myanmar') return [-12, 0];
if (d.properties.name === 'Laos') return [-12, -8];
if (d.properties.name === 'Vietnam') return [-12, -4];
if (d.properties.name === 'Japan') return [5, 5];
// Oceania
if (d.properties.name === 'Indonesia') return [0, -5];
if (d.properties.name === 'Papua New Guinea') return [-5, -10];
if (d.properties.name === 'Australia') return [-15, 0];
if (d.properties.name === 'New Zealand') return [-15, 0];
// otherwise if not specified
return [-10, 0];
})
d3.select('body')
.style('overflow', 'hidden');
const parentWidth = d3.select('body').node().getBoundingClientRect().width;
const margin = {top: 0, right: 0, bottom: 0, left: 0};
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const color = d3.scaleQuantile()
.range([
'rgb(247,251,255)',
'rgb(222,235,247)',
'rgb(198,219,239)',
'rgb(158,202,225)',
'rgb(107,174,214)',
'rgb(66,146,198)',
'rgb(33,113,181)',
'rgb(8,81,156)',
'rgb(8,48,107)',
'rgb(3,19,43)'
]);
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('class', 'map');
const projection = d3.geoRobinson()
.scale(148)
.rotate([352, 0, 0])
.translate( [width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
svg.call(tip);
queue()
.defer(d3.json, 'world_countries.json')
.defer(d3.tsv, 'world_population.tsv')
.await(ready);
function ready(error, geography, data) {
data.forEach(d => {
d[colorVariable] = Number(d[colorVariable].replace(',', ''));
})
const colorVariableValueByID = {};
data.forEach(d => { colorVariableValueByID[d[geoIDVariable]] = d[colorVariable]; });
geography.features.forEach(d => { d[colorVariable] = colorVariableValueByID[d.id] });
// calculate ckmeans clusters
// then use the max value of each cluster
// as a break
const numberOfClasses = color.range().length - 1;
const ckmeansClusters = ss.ckmeans(data.map(d => d[colorVariable]), numberOfClasses);
const ckmeansBreaks = ckmeansClusters.map(d => d3.min(d));
console.log('numberOfClasses', numberOfClasses);
console.log('ckmeansClusters', ckmeansClusters);
console.log('ckmeansBreaks', ckmeansBreaks);
// set the domain of the color scale based on our data
color
// .domain(ckmeansBreaks);
//
// .domain(jenksNaturalBreaks)
//
// .domain(d3.extent(data, d => d[colorVariable]));
//
.domain([
0,
5,
10,
20,
30,
40,
50,
60,
70,
80,
90,
]);
svg.append('g')
.attr('class', 'countries')
.selectAll('path')
.data(geography.features)
.enter().append('path')
.attr('d', path)
.style('fill', d => {
if (typeof colorVariableValueByID[d.id] !== 'undefined') {
return color(colorVariableValueByID[d.id])
}
return 'white'
})
.style('fill-opacity',0.8)
.style('stroke', d => {
if (d[colorVariable] !== 0) {
return 'white';
}
return 'lightgray';
})
.style('stroke-width', 1)
.style('stroke-opacity', 0.5)
// tooltips
.on('mouseover',function(d){
tip.show(d);
d3.select(this)
.style('fill-opacity', 1)
.style('stroke-opacity', 1)
.style('stroke-width', 2)
})
.on('mouseout', function(d){
tip.hide(d);
d3.select(this)
.style('fill-opacity', 0.8)
.style('stroke-opacity', 0.5)
.style('stroke-width', 1)
});
svg.append('path')
.datum(topojson.mesh(geography.features, (a, b) => a.id !== b.id))
.attr('class', 'names')
.attr('d', path);
}
</script>
</body>
</html>
Modified http://d3js.org/d3.v4.min.js to a secure url
Modified http://d3js.org/queue.v1.min.js to a secure url
Modified http://d3js.org/topojson.v1.min.js to a secure url
https://d3js.org/d3.v4.min.js
https://d3js.org/queue.v1.min.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/d3-geo-projection.v1.min.js
https://unpkg.com/simple-statistics@2.0.0/dist/simple-statistics.min.js
https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js