Skip to content

Instantly share code, notes, and snippets.

@nyem69
Last active February 1, 2017 19:54
Show Gist options
  • Save nyem69/527dd895d1e06989cdacc3db7b087199 to your computer and use it in GitHub Desktop.
Save nyem69/527dd895d1e06989cdacc3db7b087199 to your computer and use it in GitHub Desktop.
Dengue Cases in Malaysia
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment