govlab-styleguide
Version:
A styleguide / lightweight frontend framework for GovLab websites
362 lines (312 loc) • 11.2 kB
JavaScript
// Map II : Simplified bubble map (ODI version)
var mobileOnly = '(max-width: 767px)'
// disable the map on mobile (ie replace with something else)
if (window.matchMedia(mobileOnly).matches) {
console.log('Mobile, map disabled');
} else { // desktop
var width = 900,
height = 400,
active = d3.select(null);
var projection = d3.geo.mercator()
.scale(120)
.translate([width / 2, height / 1.5]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select('.map').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'background')
.attr('width', width)
.attr('height', height);
var g = svg.append('g')
.style('stroke-width', '1.5px');
// create a shade based on array of rgb values and scalar
function shade(rgb, v) {
for (var i in rgb) { rgb[i] = rgb[i]*v > 255 ? 255 : rgb[i]*v; }
return rgb;
}
// set up regions based on the topojson geometry id of each country
// var northAmerica = d3.set([
// 124, 840
// ]);
// var eastAsia = d3.set([
// 16, 882, 104, 116, 585, 156, 598, 242, 608, 360, 296, 90, 408, 410, 764, 418,
// 626, 458, 776, 584, 583, 548, 496, 704, 392, 36, 554, 540, 158
// ]);
// var euCentralAsia = d3.set([
// 8, 807, 51, 498, 31, 499, 112, 642, 70, 688, 100, 762, 268, 792, 398, 795, 804,
// 417, 860, 643, 703
// ]);
// var latinAmerica = d3.set([
// 84, 328, 68, 332, 76, 340, 170, 388, 188, 484, 192, 558, 212, 214, 591, 600, 218,
// 604, 222, 662, 308, 670, 320, 740, 862, 32, 858, 152, 238
// ]);
// var midEastNorthAfrica = d3.set([
// 12, 434, 262, 504, 818, 760, 364, 788, 368, 400, 887, 422, 682, 512, 784, 634, 414
// ]);
// var southAsia = d3.set([
// 4, 462, 50, 524, 64, 586, 356, 144
// ]);
// var subSaharanAfrica = d3.set([
// 24, 450, 204, 454, 72, 466, 854, 478, 108, 480, 132, 508, 120, 516, 140, 562, 566,
// 174, 646, 180, 678, 178, 686, 384, 694, 232, 706, 231, 710, 266, 728, 729, 270,
// 288, 748, 226, 834, 624, 768, 404, 800, 426, 894, 430, 716, 148, 324, 732
// ]);
// var westEurope = d3.set([
// 304, 352, 752, 578, 246, 826, 372, 250, 724, 620, 56, 528, 276, 616, 203, 40, 380,
// 300, 348, 428, 233, 208, 756, 440, 191, 705
// ]);
// var verboseNames = {
// 'northAmerica' : 'North America',
// 'eastAsia' : 'East Asia & Pacific',
// 'euCentralAsia' : 'East Europe & Central Asia',
// 'latinAmerica' : 'Latin America & Caribbean',
// 'midEastNorthAfrica' : 'Middle East & North Africa',
// 'southAsia' : 'South Asia',
// 'subSaharanAfrica' : 'Sub-Saharan Africa',
// 'westEurope' : 'West Europe'
// }
var regions =
{
'af' : {
'name' : 'Africa',
'geometries' : [12, 24, 204, 72, 854, 108, 120, 132, 140, 148, 174, 178, 180, 262, 818,
226, 232, 231, 266, 270, 288, 324, 624, 384, 404, 426, 430, 434, 450,
454, 466, 478, 480, 504, 508, 516, 562, 566, 646, 678, 686, 690, 694,
706, 710, 728, 729, 748, 834, 768, 788, 800, 894, 716],
'translate' : {x: 1, y: .9}
},
'as' : {
'name' : 'Asia',
'geometries' : [4, 48, 50, 64, 96, 116, 104, 156, 626, 86, 360, 364, 368, 376, 392, 400,
398, 408, 410, 414, 417, 418, 422, 458, 462, 496, 524, 512, 586, 608,
634, 643, 682, 702, 144, 760, 762, 764, 792, 795, 784, 860, 704, 887],
'translate' : {x: 1.5, y: 1}
},
'eu' : {
'name' : 'Europe',
'geometries' : [8, 20, 51, 40, 31, 112, 56, 70, 100, 191, 196, 203, 208, 233, 246, 250,
268, 276, 300, 348, 352, 372, 380, 428, 438, 440, 442, 807, 470, 498, 492,
499, 528, 578, 616, 620, 642, 674, 688, 703, 705, 724, 752, 756, 804, 826,
336, 304, 352],
'translate' : {x: 1, y: 1}
},
'na' : {
'name' : 'North America',
'geometries' : [28, 44, 52, 84, 124, 188, 192, 212, 214, 222, 308, 320, 332, 340, 388, 484,
558, 591, 659, 662, 670, 780, 840],
'translate' : {x: .5, y: 1.5}
},
'oc' : {
'name' : 'Oceania',
'geometries' : [36, 242, 296, 584, 583, 520, 554, 585, 598, 882, 90, 776, 548],
'translate' : {x: 1.6, y: 1}
},
'sa' : {
'name' : 'South America',
'geometries' : [32, 68, 76, 152, 170, 218, 328, 600, 604, 740, 858, 862],
'translate' : {x: 1.1, y: .9}
}
}
function ready(error, world, studies, names) {
if (error) throw error;
// get country names from topojson
var countries = topojson.feature(world, world.objects.countries).features;
countries = countries.filter(function(d) {
return names.some(function(n) {
if (d.id == n.id) return d.name = n.name;
});
}).sort(function(a, b) {
return a.name.localeCompare(b.name);
});
// draw map
// add regions
for (var r in regions) {
g.append('path')
.datum(topojson.merge(world, world.objects.countries.geometries.filter( function(d, i) { return d3.set(regions[r].geometries).has(d.id); })))
.attr('class', 'region')
.attr('id', r)
.attr('d', path)
.on("click", clicked)
.on("mouseover", highlight)
.on("mouseout", deHighlight);
}
// draw bubbles
var s = studies.children; // quick shorthand copy because we still need the original structure with children for pack layout later
// count up totals in data categories and flag data duplicates for later filtering
var counts = {}; // object for counting totals in the provided data, could possibly be merged into studies obj
for (var i in s) {
var l = s[i].location.replace(/\s+/g, '-');
if (!(l in counts)) {
counts[l] = {};
}
if ('count' in counts[l]) {
counts[l].count++;
studies.children[i].duplicate = true;
} else {
counts[l].count = 1;
}
}
// assign a size value to each datum based on the count
var base = 4; // log base for bubbles size curve
var scale = 60; // multiplier for bubbles size curve
for (var i in studies.children) {
var l = s[i].location.replace(/\s+/g, '-');
studies.children[i].size = (Math.log(counts[l].count + 1)/Math.log(base))*scale;
}
var node = svg.selectAll('svg')
.data(studies.children)
.enter().append('g')
.filter(function(d) { return !d.duplicate; })
.attr('class', function(d, i) {
return 'node';
})
.attr('transform', function(d, i) {
var region = g.select('#' + d.location.replace(/\s+/g, '-')).datum();
var b = path.bounds(region),
x = (b[0][0] + b[1][0]) / 2,
y = (b[0][1] + b[1][1]) / 2;
// manual adjustments
// (i.e. some of the bounding boxes don't make visual sense, so just
// adjust those manually)
console.log(d.location);
console.log(regions);
if (d.location in regions) {
x *= regions[d.location].translate.x;
y *= regions[d.location].translate.y;
}
// if (d.location === 'northAmerica') {
// x *= .5;
// y *= 1.5;
// } else if (d.location === 'euCentralAsia') {
// x *= 1.35;
// y *= 1.5;
// } else if (d.location === 'eastAsia') {
// x *= 1.6;
// y *= .9;
// } else if (d.location === 'latinAmerica') {
// x *= 1.2;
// y *= .95;
// } else if (d.location === 'westEurope') {
// x *= .95;
// y *= .95;
// } else if (d.location === 'subSaharanAfrica') {
// x *= 1.05;
// y *= .9;
// } else if (d.location === 'midEastNorthAfrica') {
// x *= 1.05;
// y *= .9;
// }
return 'translate(' + x + ',' + y + ')';
});
node.append('circle')
.attr('r', function(d) {
return d.size/2; // ie size is a diameter for layout purposes
})
.style('fill', function(d) {
// strip out just the count numbers and put into a flat array, then find the max
var countsarr = [];
for (var i in counts) { countsarr.push(counts[i].count); }
var max = Math.max.apply(null, countsarr);
// calculate the value of the shade (logarithmic)
var base = 2; // log base for shade curve
var scale = 3; // multiplier for shade curve
var c = counts[d.location.replace(/\s+/g, '-')].count;
var v = (Math.log(c + 1)/Math.log(base))*scale/max;
v = v > 1 ? 1 : v;
return d3.rgb.apply(null, shade([0, 138, 179], v));
})
.attr('id', function(d, i) {
return '_bubble_' + d.location.replace(/\s+/g, '-');
})
.on("click", clicked)
.on("mouseover", highlight)
.on("mouseout", deHighlight);
node.append('text')
.attr('dy', '.3em')
.style('text-anchor', 'middle')
.text(function(d) {
var t =
// d.location + ' ' +
counts[d.location.replace(/\s+/g, '-')].count;
return t;
})
.attr('id', function(d, i) {
return '_text_' + d.location.replace(/\s+/g, '-');
})
.on("click", clicked)
.on("mouseover", highlight)
.on("mouseout", deHighlight);
// no one needs you antarctica
g.select('#Antarctica').remove();
}
// this allows us to process multiple data sources in a single function using d3, e.g. instead of just d3.json()
queue()
.defer(d3.json, 'static/js/world.json')
.defer(d3.json, 'static/js/studies.json')
.defer(d3.tsv, 'static/js/world-country-names.tsv')
.await(ready);
function highlight(d) {
var region = this.id.replace(/_bubble_|_text_/, '');
var bubble = this.id.replace(/^(?!_bubble_|_text_)|_text_/, '_bubble_');
d3.selectAll('.node').classed('fade', true);
d3.select('.map-caption').text(regions[region].name);
d3.select('.map-caption').classed('default', false);
d3.select('#' + region).classed('active', true);
d3.select('#' + bubble).classed('active', true);
zoomBubble('#' + bubble, 1.4);
// console.log('in', this.id); // debug
}
function deHighlight(d) {
var region = '#' + this.id.replace(/_bubble_|_text_/, '');
var bubble = '#' + this.id.replace(/^(?!_bubble_|_text_)|_text_/, '_bubble_');
d3.selectAll('.node').classed('fade', false);
d3.select('.map-caption').text('Select a Region');
d3.select('.map-caption').classed('default', true);
d3.select(region).classed('active', false);
d3.select(bubble).classed('active', false);
zoomBubble(bubble, -1);
// console.log('out', this.id); // debug
}
var intervals = {};
function zoomBubble(elem, zoom) {
if (d3.select(elem)[0][0] === null) { return -1; }
var
frames = 100,
e = d3.select(elem),
r = Number(e.attr('r')),
eid = e.attr('id'),
x = 0
;
var defaultSize = e.datum().size/2;
function frame() {
if (zoom > 0) {
// e.attr('r', defaultSize*(1+(x/frames)*(zoom-1)));
e.attr('r', r+(zoom*defaultSize-r)*(x/frames));
x++;
if (x >= frames) {
clearInterval(id);
}
} else { // reset to original size
e.attr('r', r-(r-defaultSize)*(x/frames));
x++;
if (x >= frames) {
clearInterval(id);
}
}
}
if (eid in intervals && intervals[eid] > 0) {
clearInterval(intervals[eid]);
}
var id = setInterval(frame, 1);
intervals[eid] = id;
return id;
}
function clicked(d) {
var region = this.id.replace(/_bubble_|_text_/, '');
d3.selectAll('.region').classed('selected', false);
d3.select('#' + region).classed('selected', true);
filterBy ('region-' + region);
}
}