Inspired by
forked from saifulazfar's block: Malaysia TopoJSON
license: |
Inspired by
forked from saifulazfar's block: Malaysia TopoJSON
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Dengue Cases in Malaysia</title> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="//d3js.org/topojson.v1.min.js"></script> | |
<script src="//d3js.org/d3-queue.v3.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/Turf.js/3.0.14/turf.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js"></script> | |
<style> | |
body { | |
font-family: 'Helvetica Neue',Helvetica, Arial, sans-serif; | |
} | |
</style> | |
<body> | |
<script> | |
var comma = d3.format(','), | |
f1 = d3.format(',.1f'); | |
d3.queue() | |
.defer(d3.csv, 'https://raw.githubusercontent.com/rengaray/DengueCasesMalaysia/master/DengueCases2011to2015ByState.csv') | |
.defer(d3.json, 'https://gist.githubusercontent.com/saifulazfar/76053d7a7d420a3a0bc0fb5849006309/raw/aa842bd44eeb96e2c93a9ac074dc21d40aac8629/.MAS_MY.json') | |
.await(function(err, dengue, mas){ | |
if (err) throw err; | |
// feature collections | |
var fc = topojson.feature(mas, mas.objects.states); | |
shiftBorneo(fc, [-5.2, 0]); | |
//---------------- | |
// data join | |
//---------------- | |
dengue.forEach(function(d){ | |
d.Year = +d.Year; | |
d.Week = +d.Week; | |
d.Total_Cases = +d.Total_Cases; | |
d.Outbreak_Duration = +d.Outbreak_Duration; | |
d.date = moment().day("Monday").year(d.Year).week(d.Week); | |
}); | |
var states = d3.nest() | |
.key(function(d){ return d.State }) | |
.key(function(d){ return d.Year }) | |
.rollup(function(d){ return d3.sum(d, function(d){ return d.Total_Cases }) }) | |
.entries(dengue); | |
var maxByStateYear = 0; | |
states.forEach(function(d){ | |
maxByStateYear = d3.max([maxByStateYear, d3.max(d.values, function(d){ return d.values}) ]); | |
}); | |
var clrScale = d3.scale.sqrt() | |
.domain([0,maxByStateYear]) | |
.range(['#ffc', '#ff6100']); | |
var shadowDistance = d3.scale.sqrt() | |
.domain([0,maxByStateYear]) | |
.range([.001,.02]); | |
// append state keys to map properties | |
var stateKeys = { | |
'Wilayah Persekutuan Kuala Lumpur':'WPKL', | |
'Pulau Pinang':'P.Pinang', | |
'Negeri Sembilan':'N.Sembilan' | |
}; | |
fc.features.forEach(function(d){ | |
var mf = states.filter(function(k){ return k.key == d.id }); | |
if (mf.length) { | |
d.properties.key = mf[0].key; | |
stateKeys[d.id] = mf[0].key; | |
}else if (stateKeys[d.id]) { | |
var mf = states.filter(function(k){ return k.key == stateKeys[d.id] }); | |
if (mf.length) d.properties.key = mf[0].key; | |
else console.warn(d.id); | |
} | |
}); | |
//---------------- | |
// years | |
//---------------- | |
var years = d3.nest().key(function(d){ return d.Year }) | |
.entries(dengue); | |
years.sort(function(a,b){ return d3.descending(+a.key,+b.key) }); | |
//---------------- | |
// grids | |
//---------------- | |
var grids = { | |
width: innerWidth - 20, | |
padding: 20, | |
col: 2 | |
}; | |
grids.rows = Math.ceil(years.length/grids.col); | |
grids.cellSize = (grids.width/grids.col) - grids.padding; | |
//---------------- | |
// map sizing | |
//---------------- | |
var pp = projectionProperties(fc, grids.cellSize); | |
var projection = pp.projection; | |
var path = d3.geo.path().projection(projection); | |
grids.mapHeight = pp.height; | |
grids.height = (grids.mapHeight + (grids.padding*2)) * grids.rows; | |
//---------------- | |
// render | |
//---------------- | |
d3.select('body') | |
.call(function(sel) { | |
//---------------- | |
// title | |
//---------------- | |
sel.append('h1') | |
.style('text-align','center') | |
.html('Dengue Cases in Malaysia, '+ d3.extent(years, function(d){ return +d.key }).join(' to ') ); | |
sel.append('h2').attr('class','subtitle') | |
.style('text-align','center') | |
.call(function(sel){ | |
sel.append('span').attr('class','description').style('padding-right','5px'); | |
sel.append('span').html(':'); | |
sel.append('span').attr('class','total-all').style('padding-left','5px').html(0); | |
}); | |
//---------------- | |
// svg | |
//---------------- | |
sel.append('svg') | |
.attr({ | |
width: grids.width, | |
height: grids.height, | |
}) | |
.call(function(sel) { | |
sel.append('defs') | |
.call(function(sel) { | |
sel.append('g').attr('class','statePaths') | |
.selectAll('path').data(fc.features) | |
.enter().append('path') | |
.attr('id', function(d){ return 'state-'+ d.id.replace(/\s+/g,'_') }) | |
.attr('d', path); | |
sel.append('g').attr('class','glowFilters') | |
.selectAll('filter').data([3]) | |
.enter().append('filter') | |
.attr('id', function(d){ return "shadow-"+d }) | |
.append("feGaussianBlur") | |
.attr("stdDeviation", function(d){ return d }); | |
}); | |
var r=0; | |
sel.selectAll('.years').data(years, function(d){ return +d.key }) | |
.enter().append('g') | |
.attr({ | |
class:'years', | |
transform: function(d,i){ | |
var c = (i % grids.col) * (grids.cellSize + grids.padding); | |
var tr = 'translate('+(i % grids.col * (grids.cellSize + grids.padding))+','+(r * ((grids.mapHeight + (grids.padding*2))))+')'; | |
if (i % grids.col == grids.col-1) r++; | |
return tr; | |
} | |
}) | |
.append('g').attr('transform','translate('+grids.padding+','+grids.padding+')') | |
.call(function(sel) { | |
sel.append('rect') | |
.attr({ | |
width: grids.cellSize, | |
height: grids.mapHeight, | |
fill:'steelblue', | |
stroke:'steelblue', | |
opacity:0 | |
}) | |
.on('mouseover', initState); | |
//---------------- | |
// years & total | |
//---------------- | |
sel.append('text') | |
.attr({ | |
transform:'translate('+ (grids.cellSize/2)+',20)', | |
'text-anchor':'middle' | |
}) | |
.call(function(sel) { | |
sel.append('tspan') | |
.text(function(d){ return d.key }) | |
sel.append('tspan') | |
.attr({ | |
class:'year-cases', | |
x:0, | |
dy:'1em', | |
'font-size':'150%' | |
}) | |
.text(0); | |
}); | |
//---------------- | |
// map of states | |
//---------------- | |
sel.append('g') | |
.attr({ | |
stroke:'#999', | |
'stroke-width':.5, | |
}) | |
.selectAll('g').data(function(d){ | |
var data = []; | |
fc.features.forEach(function(k){ | |
var t = { | |
id: k.id, | |
values: d.values.filter(function(p){ return p.Year == +d.key && p.State==k.properties.key }), | |
}; | |
t.total = d3.sum(t.values, function(d){ return d.Total_Cases }); | |
data.push(t); | |
}); | |
data.sort(function(a,b){ return d3.ascending(a.total,b.total) }); | |
return data; | |
}) | |
.enter().append('g') | |
.call(function(sel) { | |
//---------------- | |
// state shadow | |
//---------------- | |
sel.append('use') | |
.attr({ | |
'class':function(d){ return d.values.length ? 'shadow' : null }, | |
'xlink:href': function(d){ return d.values.length ? '#state-'+ d.id.replace(/\s+/g,'_') : null }, | |
fill:'#666', | |
stroke:'none', | |
}); | |
//---------------- | |
// state map | |
//---------------- | |
sel.append('use') | |
.attr({ | |
class:'state', | |
'xlink:href': function(d){ return '#state-'+ d.id.replace(/\s+/g,'_') }, | |
fill:'#ddd', | |
'stroke':function(d){ return d.values.length ? null : '#fff' }, | |
}) | |
.style('cursor','pointer') | |
.on('mouseover',function(d){ | |
d3.selectAll('.state') | |
.transition().duration(333) | |
.attr('fill', function(k){ | |
return k.id==d.id | |
? k.values.length ? clrScale(k.total) : '#999' | |
: '#ddd' | |
}); | |
d3.selectAll('.year-cases').each(function(k){ | |
k._tmpValue = d3.sum( | |
k.values.filter(function(k){ return k.State==stateKeys[d.id]}), | |
function(k){ return k.Total_Cases } | |
); | |
}) | |
.transition().duration(333) | |
.tween("text", function(d) { | |
var i = d3.interpolateRound(+this.textContent.replace(/\D/g,''), d._tmpValue); | |
return function(t) {this.textContent = i(t)==0 ? '' : comma(i(t))}; | |
}); | |
d3.select('.subtitle .description').html(d.id); | |
d3.select('.subtitle .total-all') | |
.datum( d3.sum( | |
dengue.filter(function(k){ return k.State==stateKeys[d.id]}), | |
function(d){ return d.Total_Cases }) | |
) | |
.transition().duration(333) | |
.tween("text", function(d) { | |
var i = d3.interpolateRound(+this.textContent.replace(/\D/g,''), d); | |
return function(t) {this.textContent = i(t)==0 ? '' : comma(i(t))}; | |
}); | |
}); | |
//---------------- | |
// shadow effects | |
//---------------- | |
d3.select(window).on("mousemove", shadowDirection); | |
shadowDirection(); | |
function shadowDirection() { | |
var x = innerWidth / 2, | |
y = innerHeight / 2; | |
if (d3.event) { | |
x -= d3.event.clientX, | |
y -= d3.event.clientY; | |
} | |
d3.selectAll('.shadow').style(vendor + "transform", function(d){ | |
return "translateX(" + x * shadowDistance(d.total) + "px) translateY(" + y * shadowDistance(d.total) + "px)" | |
}); | |
} | |
var vendor = (function(p) { | |
var i = -1, n = p.length, s = document.body.style; | |
while (++i < n) if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-"; | |
return ""; | |
})(["webkit", "ms", "Moz", "O"]); | |
}); | |
initState(null, 500,1000); | |
//---------------- | |
// defaults | |
//---------------- | |
function initState(d, delay, duration) { | |
if (!delay) delay=0; | |
if (!duration) duration = 333; | |
d3.selectAll('.shadow') | |
.attr('filter', function(k){ | |
return 'url(#shadow-3)' | |
}); | |
d3.selectAll('.state') | |
.transition().delay(delay).duration(duration) | |
.attr('fill', function(k){ | |
return k.values.length ? clrScale(k.total) : '#ddd' | |
}); | |
d3.selectAll('.year-cases').each(function(k){ | |
k._tmpValue = d3.sum(k.values,function(k){ return k.Total_Cases }); | |
}) | |
.transition().delay(delay).duration(duration) | |
.tween("text", function(d) { | |
var i = d3.interpolateRound(+this.textContent.replace(/\D/g,''), d._tmpValue); | |
return function(t) {this.textContent = i(t)==0 ? '' : comma(i(t))}; | |
}); | |
d3.select('.subtitle .description') | |
.html(' Total'); | |
d3.select('.subtitle .total-all') | |
.datum( d3.sum(dengue, function(d){ return d.Total_Cases }) ) | |
.transition().delay(delay).duration(duration) | |
.tween("text", function(d) { | |
var i = d3.interpolateRound(+this.textContent.replace(/\D/g,''), d); | |
return function(t) {this.textContent = i(t)==0 ? '' : comma(i(t))}; | |
}); | |
} | |
}); | |
}); | |
}); | |
d3.select(self.frameElement).style("height", grids.height + "px"); | |
}); | |
//---------------- | |
// find projection scale & translate and map height to fit map width | |
// http://stackoverflow.com/questions/14492284/center-a-map-in-d3-given-a-geojson-object#14691788 | |
//---------------- | |
function projectionProperties(fc, width, height) { | |
if (!width) width = 960; | |
// Create a unit projection. | |
var projection = d3.geo.mercator() | |
.scale(1) | |
.translate([0, 0]); | |
// Create a path generator. | |
var path = d3.geo.path() | |
.projection(projection); | |
// calculate height | |
var bb = turf.bbox(fc), | |
bbWidth = bb[2]-bb[0], | |
bbHeight = bb[3]-bb[1], | |
heightRatio = bbHeight/bbWidth; | |
if (!height) height = width * heightRatio; | |
// Compute the bounds of a feature of interest, then derive scale & translate. | |
var b = path.bounds(fc), | |
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height), | |
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2]; | |
// Update the projection to use computed scale & translate. | |
projection | |
.scale(s) | |
.translate(t); | |
return { | |
width: width, // map pixel width | |
height: height, // map pixel height | |
projection: projection | |
}; | |
} | |
//---------------- | |
// simple shifting of borneo to be closer to peninsula | |
// e.g: shiftBorneo(fc, [-6.7, -.6]); | |
// shiftBorneo(fc, [-5.2, 0]); | |
//---------------- | |
function shiftBorneo(fc, shiftLngLat) { | |
function shiftArray(d) { | |
d.forEach(function(d){ | |
if (typeof d[0]=='number') { | |
d[0] = +d[0] + shiftLngLat[0]; | |
d[1] = +d[1] + shiftLngLat[1]; | |
}else { | |
shiftArray(d); | |
} | |
}); | |
} | |
fc.features.forEach(function(d){ | |
if (turf.bbox(d)[0] > 105) { | |
d.geometry.coordinates.forEach(function(d){ | |
shiftArray(d); | |
}); | |
} | |
}); | |
} | |
</script> |