Last active
December 11, 2017 16:48
-
-
Save tonmcg/bb353738d84198b2865760a3f667590d to your computer and use it in GitHub Desktop.
Africa Choropleth Map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: no | |
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<!--https://bl.ocks.org/bricedev/3905007f1794b0cb0bcd--> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Shiny Example | Africa Lambert Conformal Conic</title> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora"> | |
<style> | |
body { | |
font-family: 'Lora', serif; | |
} | |
.country-border { | |
fill: none; | |
stroke: #000000; | |
stroke-width: 0.5px; | |
pointer-events:all; | |
stroke-linejoin: round; | |
stroke-linecap: round; | |
} | |
.background { | |
fill: #f5f5f5; | |
fill-opacity: 0.5; | |
} | |
.continent { | |
fill: #222; | |
stroke: #fff; | |
} | |
.continent :hover { | |
fill: #8a8a8a; | |
} | |
.tooltip { | |
top: 100px; | |
left: 100px; | |
-moz-border-radius: 5px; | |
border-radius: 5px; | |
/*border: 2px solid #000;*/ | |
background: #333; | |
opacity: .9; | |
color: white; | |
padding: 10px; | |
min-width: 375px; | |
min-width: 36.75vmin; | |
font-size: 2.25vmin; | |
line-height: 24pt; | |
font-family: 'Lora', serif; | |
font-weight: lighter; | |
visibility: visible; | |
} | |
.tooltip.right::before { | |
content: ''; | |
display: block; | |
width: 0; | |
height: 0; | |
position: absolute; | |
opacity: .9; | |
border-top: 8px solid transparent; | |
border-bottom: 8px solid transparent; | |
border-left: 8px solid #333; | |
right: -8px; | |
top: 65px; | |
/* arbitrarily set */ | |
} | |
.tooltip.left::before { | |
content: ''; | |
display: block; | |
width: 0; | |
height: 0; | |
position: absolute; | |
opacity: .9; | |
border-top: 8px solid transparent; | |
border-bottom: 8px solid transparent; | |
left: -8px; | |
border-right: 8px solid #333; | |
top: 65px; | |
/* arbitrarily set */ | |
} | |
/* SVG Scaling */ | |
/*body,*/ | |
/*svg {*/ | |
/* height: 100%;*/ | |
/* margin: 0;*/ | |
/* width: 100%;*/ | |
/* float: left;*/ | |
/*}*/ | |
.scaling-svg-container { | |
position: relative; | |
height: 0; | |
width: 100%; | |
padding: 0 | |
} | |
.scaling-svg { | |
position: absolute; | |
height: 99%; | |
width: 100%; | |
left: 0; | |
top: 0 | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-12 col-md-12"> | |
<div id="title" class="page-header"> | |
<h2>Africa Lambert Conformal Conic <small>Incorporating R, Shiny, and D3.js</small></h2> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-xs-4 col-md-4"> | |
<div class="form-group"> | |
<label for="dimensions" class="col-form-label">Factor </label> | |
<select class="form-control" id="dimensions"></select> | |
</div> | |
</div> | |
<div class="col-xs-8 col-md-8"></div> | |
</div> | |
<div class="row" id="canvas"> | |
<div class="col-md-9 col-xs-9"> | |
<div class="scaling-svg-container"> | |
<svg></svg> | |
</div> | |
</div> | |
<div class="col-md-3 col-xs-3"> | |
<div id="textDescription" style="height:250px;"> | |
<h3></h3> | |
<p></p> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-xs-12 col-md-12" id="source"> | |
<footnote></footnote> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-xs-12 col-md-12"> | |
<footer> | |
<span>Visualization by <a href="https://twitter.com/tonmcg">Tony McGovern</a></span> | |
</footer> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.0.1/spin.min.js" type="text/javascript"></script> | |
<script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js" type="text/javascript"></script> | |
<script src="https://unpkg.com/topojson-client@3" type="text/javascript"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.11.0/d3-legend.js" type="text/javascript"></script> | |
<script defer type="text/javascript"> | |
// default view for map | |
let measure = 'mortalityrate'; | |
const cols = { | |
"mortalityrate": { | |
"label": "Mortality Rate", | |
"type": "number", | |
"format": ",.0f", | |
"title": "Mortality rate, under-5 (per 1,000 live births)", | |
"latestyear": "2016", | |
"url": "http://www.childmortality.org/", | |
"source": "childmortality.org", | |
"description": "Estimates Developed by the UN Inter-agency Group for Child Mortality Estimation ( UNICEF, WHO, World Bank, UN DESA Population Division ). Projected data are from the United Nations Population Division's World Population Prospects; and may in some cases not be consistent with data before the current year." | |
}, | |
"dyingyoung": { | |
"label": "Probability Dying Young", | |
"type": "percentage", | |
"format": ".1%", | |
"title": "Probability of dying at age 5-14 years (per 1,000 children age 5)", | |
"latestyear": "2016", | |
"url": "http://www.childmortality.org/", | |
"source": "childmortality.org", | |
"description": "Estimates Developed by the UN Inter-agency Group for Child Mortality Estimation ( UNICEF, WHO, World Bank, UN DESA Population Division )" | |
} | |
} | |
// spinner loader settings | |
const opts = { | |
lines: 9, // The number of lines to draw | |
length: 9, // The length of each line | |
width: 5, // The line thickness | |
radius: 14, // The radius of the inner circle | |
color: '#c10e19', // #rgb or #rrggbb or array of colors | |
speed: 1.9, // Rounds per second | |
trail: 40, // Afterglow percentage | |
className: 'spinner', // The CSS class to assign to the spinner | |
}; | |
// create spinner | |
let target = d3.select("body").node(); | |
// trigger loader | |
let spinner = new Spinner(opts).spin(target); | |
// create tooltip | |
let tooltip = d3.select("body").append("div").style("position", "absolute").style("z-index", "10").style("visibility", "hidden").attr("class", "tooltip"); | |
// select element | |
let measureSelect = d3.select('#dimensions'); | |
let width = 960; | |
let height = 720; | |
let projection = d3.geoMercator() | |
.scale(410) | |
.translate([width / 3, height / 2]); | |
let path = d3.geoPath() | |
.projection(projection); | |
// use Susie Lu's d3-legend plugin | |
// http://d3-legend.susielu.com/ | |
let d3legend = d3.legendColor() | |
.shapeWidth(width / 10) | |
.cells(9) | |
.orient("horizontal") | |
.labelOffset(3) | |
.ascending(true) | |
.labelAlign("middle") | |
.shapePadding(2); | |
let svg = d3.select("svg") | |
.attr("width", width) | |
.attr("height", height); | |
// create options for select element | |
var selectEnter = measureSelect.selectAll('option').data(d3.keys(cols).sort()).enter(); | |
var selectUpdate = selectEnter.append('option'); | |
selectUpdate.attr('value', function(d) { | |
return d; | |
}).text(function(d) { | |
return cols[d].title; | |
}).attr('selected', function(d) { | |
if (d == measure) { | |
return true; | |
} | |
}); | |
let countryObj = {}; | |
function createMap(error, africa, data) { | |
if (error) throw error; | |
// stop spin.js loader | |
spinner.stop(); | |
data.forEach(function(d){ | |
d.mortalityrate = +d['Mortality Rate']; | |
d.dyingyoung = +d['Prob of Dying']; | |
countryObj[d.adm0_a3_is] = d; | |
}); | |
let centered; | |
// render map colors based on data | |
function renderMap() { | |
// counter for missing counties (usually in Alaska) | |
let countMissing = 0; | |
let extent = d3.extent(data, function(d) { | |
return +countryObj[d.adm0_a3_is][measure]; | |
}); | |
let color = d3.scaleSequential().interpolator(d3.interpolateOranges); | |
color.domain([extent[0], extent[1]]); | |
d3legend | |
.labelFormat(function(d) { | |
let value; | |
if (cols[measure].type == 'percentage') { | |
value = (d / 100); | |
} | |
else { | |
value = d; | |
} | |
return d3.format(cols[measure].format)(value); | |
}) | |
.title(cols[measure].label) | |
.scale(color); | |
// if legend already exists, remove and create again | |
svg.select('.legend').remove(); | |
// create legend | |
let legend = svg.append("g") | |
.attr("class", "legend") | |
.attr("transform", "translate(" + (width / 24) + "," + (height * 6 / 7) + ")"); | |
legend | |
.call(d3legend); | |
countryPath | |
.transition() | |
.duration(1000) | |
.style("fill", function(d) { | |
let country = countryObj[d.properties.adm0_a3_is]; | |
if(country && country[measure] === null) { | |
console.log(country.Country + " does not have data"); | |
return '#ccc'; | |
} | |
else if (country) { | |
return color(country[measure]); | |
} | |
else { | |
countMissing++; | |
console.log(d.properties.formal_en + " not found. Error #" + countMissing); | |
return '#ccc'; | |
} | |
}); | |
} | |
// define zoom function | |
function zoomed() { | |
group.attr("transform", d3.event.transform); | |
// group.select(".nation").style("stroke-width", 0.5 / d3.event.scale + "px"); | |
// group.select(".state-border").style("stroke-width", 0.5 / d3.event.scale + "px"); | |
// group.select(".country-border").style("stroke-width", 0.1 / d3.event.scale + "px"); | |
} | |
// When clicked, zoom in | |
function clicked(d) { | |
var x, y, k; | |
// Compute centroid of the selected path | |
if (d && centered !== d) { | |
// if (d) { | |
var centroid = path.centroid(d); | |
x = centroid[0]; | |
y = centroid[1]; | |
// k = zoom.scaleExtent()[1]; | |
k = 10; | |
centered = d; | |
} | |
else { | |
x = width / 2; | |
y = height / 2; | |
k = 1; | |
centered = null; | |
} | |
// Manually Zoom | |
svg.transition() | |
.duration(750) | |
.call(zoom.transform, d3.zoomIdentity | |
.translate(width / 2, height / 2) | |
.scale(k) | |
.translate(-x, -y)); | |
} | |
// create background box for zoom | |
svg.append("rect") | |
.attr("class", "background") | |
.attr("width", width) | |
.attr("height", height); | |
let zoom = d3.zoom() | |
.scaleExtent([1, 15]) | |
.on("zoom", zoomed); | |
svg.style("pointer-events", "all") | |
.call(zoom); | |
let group = svg.append("g") | |
.attr("class","continent"); | |
let countryPath = group.selectAll(".countries") | |
.data(topojson.feature(africa, africa.objects.collection).features) | |
.enter() | |
.append('path') | |
.attr("class", "country-border") | |
.on("click", clicked) | |
.attr("d", path); | |
renderMap(); | |
renderText(); | |
bindHover(); | |
setResponsiveSVG(); | |
measureSelect.on('change', function(d) { | |
measure = this.value; | |
renderMap(); | |
renderText(); | |
}); | |
} | |
// change factor description | |
function renderText() { | |
let textHeader = d3.select('#textDescription h3'); | |
let textDescription = d3.select('#textDescription p'); | |
let textFootnote = d3.select('#source > footnote'); | |
let title = cols[measure].title; | |
let description = cols[measure].description; | |
let url = cols[measure].url; | |
let source = cols[measure].source; | |
textHeader.style("opacity", 0).transition().duration(1000).style("opacity", 1).text(title); | |
textDescription.style("opacity", 0).transition().duration(1000).style("opacity", 1).text(description); | |
textFootnote.html('<em>Source: </em><span><a href="' + url + '" target="_blank">' + source + '</span></a>'); | |
} | |
// define mouseover and mouseout events | |
// to ensure mouseover events work on IE | |
function bindHover() { | |
document.body.addEventListener('mousemove', function(e) { | |
if (e.target.nodeName == 'path') { | |
let d = d3.select(e.target).data()[0].properties; | |
let value = countryObj[d.adm0_a3_is][measure]; | |
let content = ''; | |
if (value === null) { | |
content = countryObj[d.adm0_a3_is].geounit + ': ' + 'No data'; | |
} else { | |
content = "<b>" + d.admin + '</b><br>' + "Income Group: " + d.income_grp + '<br>' + 'Economy: ' + d.economy + '<br>' + 'Mortality Rate: ' + countryObj[d.adm0_a3_is]['mortalityrate'] + '<br>' + 'Probability of Dying Young: ' + d3.format('.1%')(countryObj[d.adm0_a3_is]['dyingyoung']/100); | |
} | |
showDetail(e, content); | |
} | |
}); | |
document.body.addEventListener('mouseout', function(e) { | |
if (e.target.nodeName == 'path') hideDetail(); | |
}); | |
} | |
// Show tooltip on hover | |
function showDetail(event, content) { | |
// show tooltip with information from the __data__ property of the element | |
let x_hover = 0; | |
let y_hover = 0; | |
let tooltipWidth = parseInt(tooltip.style('width')); | |
let tooltipHeight = parseInt(tooltip.style('height')); | |
let classed, notClassed; | |
if (event.pageX > document.body.clientWidth / 2) { | |
x_hover = tooltipWidth + 30; | |
classed = 'right'; | |
notClassed = 'left'; | |
} | |
else { | |
x_hover = -30; | |
classed = 'left'; | |
notClassed = 'right'; | |
} | |
y_hover = (document.body.clientHeight - event.pageY < (tooltipHeight + 4)) ? event.pageY - (tooltipHeight + 4) : event.pageY - tooltipHeight / 2; | |
return tooltip | |
.classed(classed, true) | |
.classed(notClassed, false) | |
.style("visibility", "visible") | |
.style("top", y_hover + "px") | |
.style("left", (event.pageX - x_hover) + "px") | |
.html(content); | |
} | |
// Hide tooltip on hover | |
function hideDetail() { | |
// hide tooltip | |
return tooltip.style("visibility", "hidden"); | |
} | |
// Many browsers -- IE particularly -- will not auto-size inline SVG | |
// IE applies default width and height sizing | |
// padding-bottom hack on a container solves IE inconsistencies in size | |
// https://css-tricks.com/scale-svg/#article-header-id-10 | |
function setResponsiveSVG() { | |
let width = +d3.select('svg').attr('width'); | |
let height = +d3.select('svg').attr('height'); | |
let calcString = +(height / width) * 100 + "%"; | |
let svgElement = d3.select('svg'); | |
let svgParent = d3.select(d3.select('svg').node().parentNode); | |
svgElement | |
.attr('class', 'scaling-svg') | |
.attr('preserveAspectRatio', 'xMinYMin') | |
.attr('viewBox', '0 0 ' + width + ' ' + height) | |
.attr('width', null) | |
.attr('height', null); | |
svgParent.style('padding-bottom', calcString); | |
} | |
d3.queue() | |
.defer(d3.json, "https://gist.githubusercontent.com/DimsumPanda/88aa1d70cd0c394feeae/raw/192d7eed828268ea91b24c100063992042f86835/africa.topojson") | |
.defer(d3.csv, "https://raw.githubusercontent.com/tonmcg/shiny_maps/master/www/data.csv") | |
.await(createMap); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment