")
$('.map-data-country').each(function(index) {
var countryName = $(this).attr('data-country');
var scrollTop = $(this).offset().top - 10;
var item = $("
").text(countryName);
console.log("scrollTop for " + countryName + ": " + scrollTop)
// I can't seem to pass in scrollTop as a param,
// so we need to use the closure
$(item).click(function() {
jqscrollTo(scrollTop)
});
jumper.append(item);
});
$('.map-data-country h2').each(function (index) {
var topLink = $("
")
.text("(back to top)")
.addClass('back-to-top');
$(topLink).click(function () {
jqscrollTo(jumper.offset().top - 400);
})
$(this).before(topLink);
});
}
function jqscrollTo(scrollTop) {
log.debug(scrollTop)
$('html,body').animate({
scrollTop: scrollTop
});
}
var topo, projection, svg, g, country_info, width, height, tooltip,
ranges, clicked_country, zoom, zoomLevel, zoomTouch, data;
jQuery(document, WOF).ready(function($) {
width = $('#container').attr('offsetWidth');
height = width / 2;
active = d3.select(null);
// set up tool tip to edit later
var tooltip = d3.select("#container")
.append("div")
.attr("class", "tooltip hidden");
var tooltipOffsetLeft = $("#container")
.attr('offsetLeft') + 40;
var tooltipOffsetTop = $("#container")
.attr('offsetTop') + 20;
// set up tool tip to edit later
var control = d3.select("#container")
.append("div")
.attr("class", "controls")
.html("")
.on('click', reset);
control.style({
'top': $("#container").attr('offsetTop') + (height * 0.9) + 'px',
'left': $("#container").attr('offsetLeft') + 10 + 'px',
'position': 'absolute',
});
function setup(width, height) {
// work out which projection to read, where to centre the map, and how
// big it should be
projection = d3.geo.robinson()
.scale(155) // scale the map to an appropriate size to fit in the canvas
.translate([width / 2, height / 1.7]); // move to middle of the canvas
// store path
path = d3.geo.path()
.projection(projection);
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 8])
.on("zoom", zoomed);
// finally create our 'canvas' for all of this to take place on
svg = d3.select("#container") // select our container div
.append("svg")
.attr("width", width)
.attr("height", height)
.attr('overflow', 'hidden')
.on('click', stopped, true);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
// add drag behaviour to the map
// TODO: find out why this breaks text select behaviour after clicking on
// a country
svg.call(zoom);
if ($('#container').attr('data-map-centre')) {
startWithZoomedMap();
}
// add group for holding the world
g = svg.append("g");
WOF.g = g;
}
// takes the topojson represention of each country in world,
// and obesity data for each country, merges them before
// rendering on screen
function ready(error, world, country_data) {
var countries = topojson.feature(world, world.objects.countries).features;
for (var j = 0; j < countries.length; j++) {
// TODO: create a new topojson dump with country names _and_ iso codes,
// to replace the current one.
country_values = [
"3 letter code: " + countries[j].id,
"2 letter code from IASO: " + countries[j].properties.id,
"name: " + countries[j].properties.name
]
if (!countries[j].properties.id) {
problemCountries.push(countries[j]);
}
if (countries[j].properties.id) {
goodcountries.push(countries[j]);
}
if (!countries[j].properties.hover) {
nohoverCountries.push(countries[j]);
}
}
WOF.allCountryData = country_data;
WOF.allCountryFeatures = countries;
WOF.draw = draw;
// put data on map
draw();
$('#map-data-container').appendTo('#extra-content');
$('#map-data-container').append("
Select a country above for further details
");
if(window.location.hash) {
var countryId = window.location.hash.split('=')[1]
WOF.selectCountry(countryId);
}
}
WOF.findCountry = function(countryId) {
return _.find( WOF.annotatedCountries, function(country) {
return country.id == countryId;
});
}
WOF.resetMap = function(fillValueCallback) {
WOF.g.html('');
WOF.draw(fillValueCallback);
WOF.triggerZoom();
}
WOF.triggerZoom = function() {
zoom.event(svg);
}
// draws map on svg canvas, based on merged country data
function draw(fillValueCallback) {
ranges = buildRanges();
var countries = WOF.annotatedCountries = annotateTopography(WOF.allCountryFeatures, WOF.allCountryData, fillValueCallback);
// add a countries group, to contain ech country
// bind dataset to the collection of country elements
// var country = svg.selectAll(".country").data(countries);
var country_group = g.append('g')
.attr("id", countries)
.selectAll(".country")
.data(countries)
country_group.enter()
.insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d, i) {
return d.id;
})
.style('fill', function(d) {
var col = fillBasedOnRange(d.properties.name, d.properties.data);
return col;
})
.call(zoom)
//tooltips
country_group
.on("mouseover", hovered)
.on("mouseout", function(d, i) {
tooltip.classed("hidden", true)
})
.on('click', clicked)
// this triggers the click, but we end up with a messy zoom
.on('touchstart', logTouchCoordsStart)
// get rid of the ugly zoom when you first touch a country
.on('touchstart.zoom', null)
.on('touchend', logTouchCoordsEnd)
WOF.setActiveClasses();
}
WOF.setActiveClasses = function() {
if(WOF.activeCountry) {
var selector = '#' + WOF.activeCountry;
d3.selectAll('svg .country').classed("inactive", true);
d3.select(selector).classed("inactive", false);
d3.select(selector).classed("active", true);
} else {
d3.selectAll('svg .country').classed("inactive", false);
d3.selectAll('svg .country').classed("active", false);
}
};
function annotateTopography(topoCountries, countryData, fillValueCallback) {
var countries = topoCountries;
for (var i = 0; i < countryData.length; i++) {
var cd = countryData[i]
for (var c = 0; c < countries.length; c++) {
if (cd.iso3166_a3 == countries[c].id) {
var data;
if(typeof fillValueCallback == 'function') {
data = fillValueCallback(countries[c].id)
} else {
data = cd.data;
}
countries[c].properties.hover = cd.hover;
countries[c].properties.id = cd.id;
countries[c].properties.name = cd.name;
countries[c].properties.data = data;
countries[c].properties.payload = cd.payload;
}
}
}
return topoCountries;
}
function logTouchCoordsStart(d) {
// console.log("TouchStart object", d)
// console.log("TouchStart:", d3.event)
if (d3.event.targetTouches) {
// console.log("TouchStart: ", d3.event.targetTouches[0].clientX, d3.event.targetTouches[0].clientY);
zoomTouch = [d3.event.targetTouches[0].clientX, d3.event.targetTouches[0].clientY];
}
// console.log("touchstarted:", d3.touches(d))
}
function logTouchCoordsEnd(d) {
if (zoomTouch[0] === d3.event.changedTouches[0].clientX) {
clicked(d)
}
}
function buildRanges(argument) {
var ranges = [];
// fetch range from the available to us
var rangesDOM = d3.selectAll('.ranges div');
$.each(rangesDOM[0], function(index, range) {
var $r = $(range);
ranges.push({
value: $r.attr('data-value'),
color: $r.attr('data-colour')
});
});
return ranges
}
// Pass in name and value, to have the matching colour returned
function fillBasedOnRange(name, value) {
// if the value is null, we have a country for which
// we don't have data, or the country code doesn't match one
// in the database
if (!value) {
problemCountries.push([name, value]);
return "#ddd";
}
for (var i = 0; i < ranges.length; i++) {
var lowerBound = ranges[i].value;
if (i + 1 < ranges.length) {
var upperBound = ranges[i + 1].value
}
// if it's lower than the lower bound of the first bucket
// serve a fallback grey
if (i === 0 && value < lowerBound) {
// console.log("Lowest bucket. limit: " + lowerBound, "value: " + value)
problemCountries.push([name, value])
return "#ddd";
}
// and if the value is higher than lower bound of our final bucket
// use that colour
if (i + 1 == ranges.length) {
if (value >= lowerBound) {
// console.log("highest bucket. limit: " + lowerBound, "value: " + value)
return "#" + ranges[i].color;
}
}
// if the value is between the lower and upper bound of the
// current bucket, shade it accordingly
if (value >= lowerBound && value < upperBound) {
// console.log("middle bucket. limit: " + lowerBound, "value: " + value, "range: " + ranges[i])
return "#" + ranges[i].color;
}
}
}
function startWithZoomedMap() {
// Fetch latlng info for a map from the DOM, and zooms the map to the
// corresponding place and zoom level.
// Used to zoom a map into a given region on load.
var zoomOutCentre = $('#container').attr('data-map-centre');
var zoomOutScale = $('#container').attr('data-map-scale');
var scale_multiple = zoomOutScale / 100;
var zoomCoords = zoomOutCentre.split(',');
var projectedZoomCoords = projection(
[(zoomCoords[1]), (zoomCoords[0])]
);
var new_x_midpoint = width / 2 - scale_multiple * projectedZoomCoords[0];
var new_y_midpoint = height / 2 - scale_multiple * projectedZoomCoords[1];
var new_translate = [new_x_midpoint, new_y_midpoint];
WOF.new_translate = new_translate;
WOF.scale_multiple = scale_multiple;
svg.transition()
.duration(1400)
.call(zoom.translate(new_translate).scale(scale_multiple).event);
}
function hovered(d) {
function offScreenBottom() {
var distanceFromScreenBottom = window.innerHeight - d3.event.clientY
return distanceFromScreenBottom < tooltipOffsetTop;
}
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
var mouseLeft = mouse[0] + tooltipOffsetLeft;
var mouseTop = mouse[1] + tooltipOffsetTop;
tooltip.classed("hidden", false)
.style({
left: +mouseLeft + "px",
top: +mouseTop + "px"
});
log.debug(d)
showPanelForTooltip(d, tooltip);
// move this to the left hand side if we're on the right hand side
if (mouse[0] > (width / 2)) {
// debugger
var tooltipWidth = parseInt(tooltip.style('width'));
tooltip.style({
left: tooltip.style({
'left': (mouseLeft - tooltipWidth) + 'px'
})
})
}
if(offScreenBottom()) {
var tooltipHeight = parseInt(tooltip.style('height'));
tooltip.style({
top: (mouseTop - tooltipHeight - 50) + 'px'
});
}
}
function chartingPayloadPresent(payload) {
// quit early if we don't have charting functions present
if (typeof buildSummaryChart !== 'function'){
return false;
}
if (!payload){
return false;
}
// for most countries this should be an ok for charting data
if (!jQuery.isEmptyObject(payload[0])){
return true
}
}
function showPanelForTooltip(d, tooltip){
// filter out empty payload members (i.e countries within within sovereign
// states, that don't have data)
var payload = _.filter(d.properties.payload, function(p){
return !jQuery.isEmptyObject(p);
});
if(chartingPayloadPresent(payload)) {
log.debug("we have a payload!")
log.debug(d)
if(payload[0].chart_image_url) {
log.info("we have an uploaded chart");
tooltip.html('
' + d.properties.name + ':
')
return true
}
// if have a data to build a chart from
if(payload[0]){
log.info("we have a chart to build:", d.id)
tooltip.html("")
tooltip.style('width', "400px");
selectionString = ".tooltip .barchart";
buildSummaryChart(selectionString, payload[0], d.properties.name);
return true
}
} else {
log.debug("No payload");
log.debug(d)
if(d.properties.hover) {
log.debug("But we have a hover")
if (typeof WOF.showPolicyMapPopup == 'function'){
WOF.showPolicyMapPopup(d, tooltip, payload)
return true
} else{
tooltip.html(d.properties.hover.split("\n").join(' '));
return true
}
}
if(d.properties.name) {
tooltip.html('
' + d.properties.name + ': No information.
');
return true
} else {
log.debug("no name here")
tooltip.classed("hidden", true);
return false
}
}
}
function clicked(d) {
var t;
if (d3.event.type === 'touchend') {
t = d3.event.changedTouches[0].target;
}
// this is the problem line
// log.info(d)
// log.info(d3.event);
if (d3.event.type === 'click') {
t = d3.event.target;
}
log.debug("Click registered:", d.id);
selectCountry(d.id);
WOF.setCountryHash();
}
function selectCountry(countryId) {
var d = WOF.findCountry(countryId);
WOF.activeCountry = countryId;
WOF.setActiveClasses();
// now, use the bounds to work out how far we zoom:
// found the top, left
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
// dx and dy are height and width of the bounding box
// x, y are the coordinates for the centre of the bounding box
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
// return the largest of the two fractions, so you can scale
// in, without zooming to close into long or thin shapes
scale = 0.9 / Math.max(dx / width, dy / height),
// scale = 1
// take our midpoint for the larger map, and subtract the difference , and
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.transition()
.duration(750)
.call(zoom.translate(translate).scale(scale).event);
var map_id = $('h3.map-title').attr('data-map');
// fetch the html to load into the page
showStaticOverride(countryId, map_id)
}
WOF.selectCountry = selectCountry;
WOF.setCountryHash = function() {
window.location.hash = 'country=' + WOF.activeCountry;
}
function showStaticOverride(countryId, map_id){
$("div#map-data-area")
.load("/maps/ajax/" + map_id + "/" + countryId, function() {
$(".map-data-content .instructions").hide();
//scroll down
var scrollTop = $("div.map-container").offset().top - 10;
// provides buffer in viewport
$('html,body').animate({
scrollTop: scrollTop
}, 500);
WOF.filterDetailPanel();
});
}
function reset() {
// resets the zoom of the map the original scale, and
// area of focus as when the map was first loaded
var new_translate = WOF.new_translate || 0;
var scale_multiple = WOF.scale_multiple || 1;
// console.log("reset the map please")
svg.transition()
.duration(750)
.call(zoom.translate(new_translate).scale(scale_multiple).event);
}
// adjust thickness of lines to match the level of zoom,
// and move the object around, based on drag movements
// TODO fix this problem function to allow us to highlight things again!
function zoomed() {
g.selectAll('.country').style("stroke-width", 1 / d3.event.scale + "px");
if (!isNaN(d3.event.translate[0])){
g.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
}
}
// If the drag behavior prevents the default click,
// also stop propagation so we don’t click-to-zoom.
function stopped() {
if (d3.event.defaultPrevented) d3.event.stopPropagation();
}
// clean up formatting of hoverString, to keep compatibility with Flash
function cleanString(hoverString) {
if (hoverString !== "" && hoverString) {
var str = hoverString.split('\n');
arr = $.map(str, function(val, i) {
return "
" + val + "
";
});
hoverString = arr.join().replace(/,/g, '');
}
if (!hoverString) {
hoverString = "";
}
return hoverString;
}
function addSelfReportDropDown(){
$('#map-data-container').click(function(ev){
// this version of jquery can't do $(this).nearest('.self-reported'),
// if the target is the self report p tag
if ($(ev.target).parent().hasClass('self-reported')) {
ev.stopPropagation();
ev.preventDefault();
$(ev.target).parent().find('.explanation').toggle('slow');
}
// if it's the self report link inside the p tag
if ($(ev.target).parent().parent().hasClass('self-reported')) {
ev.stopPropagation();
ev.preventDefault();
$(ev.target).parent().parent().find('.explanation').toggle('slow');
}
});
}
setup(width, height);
var countryDataUrl = $('h3.map-title').attr('data-file-url');
log.setLevel(log.levels.DEBUG);
addSelfReportDropDown();
queue()
.defer(d3.json, "world-topo-smaller.json")
.defer(d3.json, countryDataUrl)
.await(ready);
});