Skip to content

Instantly share code, notes, and snippets.

@tonmcg
Last active December 11, 2017 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tonmcg/bb353738d84198b2865760a3f667590d to your computer and use it in GitHub Desktop.
Save tonmcg/bb353738d84198b2865760a3f667590d to your computer and use it in GitHub Desktop.
Africa Choropleth Map
border: no
license: gpl-3.0
<!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