Skip to content

Instantly share code, notes, and snippets.

@lorenzopub
Created April 22, 2017 04:04
Show Gist options
  • Save lorenzopub/a7c6016c6c1286342d7cf9cd1d142ee0 to your computer and use it in GitHub Desktop.
Save lorenzopub/a7c6016c6c1286342d7cf9cd1d142ee0 to your computer and use it in GitHub Desktop.
One Laptop Per Child #D3makeover
license: mit
/**
* d3.tip
* Copyright (c) 2013 Justin Palmer
*
* Tooltips for d3.js SVG visualizations
*/
// eslint-disable-next-line no-extra-semi
;(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module with d3 as a dependency.
define([
'd3-collection',
'd3-selection'
], factory)
} else if (typeof module === 'object' && module.exports) {
/* eslint-disable global-require */
// CommonJS
var d3Collection = require('d3-collection'),
d3Selection = require('d3-selection')
module.exports = factory(d3Collection, d3Selection)
/* eslint-enable global-require */
} else {
// Browser global.
var d3 = root.d3
// eslint-disable-next-line no-param-reassign
root.d3.tip = factory(d3, d3)
}
}(this, function(d3Collection, d3Selection) {
// Public - contructs a new tooltip
//
// Returns a tip
return function() {
var direction = d3TipDirection,
offset = d3TipOffset,
html = d3TipHTML,
node = initNode(),
svg = null,
point = null,
target = null
function tip(vis) {
svg = getSVGNode(vis)
if (!svg) return
point = svg.createSVGPoint()
document.body.appendChild(node)
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments)
if (args[args.length - 1] instanceof SVGElement) target = args.pop()
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop ||
document.body.scrollTop,
scrollLeft = document.documentElement.scrollLeft ||
document.body.scrollLeft
nodel.html(content)
.style('opacity', 1).style('pointer-events', 'all')
while (i--) nodel.classed(directions[i], false)
coords = directionCallbacks.get(dir).apply(this)
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px')
return tip
}
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl()
nodel.style('opacity', 0).style('pointer-events', 'none')
return tip
}
// Public: Proxy attr calls to the d3 tip container.
// Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
// eslint-disable-next-line no-unused-vars
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n)
}
var args = Array.prototype.slice.call(arguments)
d3Selection.selection.prototype.attr.apply(getNodeEl(), args)
return tip
}
// Public: Proxy style calls to the d3 tip container.
// Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
// eslint-disable-next-line no-unused-vars
tip.style = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n)
}
var args = Array.prototype.slice.call(arguments)
d3Selection.selection.prototype.style.apply(getNodeEl(), args)
return tip
}
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction
direction = v == null ? v : functor(v)
return tip
}
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset
offset = v == null ? v : functor(v)
return tip
}
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html
html = v == null ? v : functor(v)
return tip
}
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if (node) {
getNodeEl().remove()
node = null
}
return tip
}
function d3TipDirection() { return 'n' }
function d3TipOffset() { return [0, 0] }
function d3TipHTML() { return ' ' }
var directionCallbacks = d3Collection.map({
n: directionNorth,
s: directionSouth,
e: directionEast,
w: directionWest,
nw: directionNorthWest,
ne: directionNorthEast,
sw: directionSouthWest,
se: directionSouthEast
}),
directions = directionCallbacks.keys()
function directionNorth() {
var bbox = getScreenBBox()
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
}
}
function directionSouth() {
var bbox = getScreenBBox()
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
}
}
function directionEast() {
var bbox = getScreenBBox()
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
}
}
function directionWest() {
var bbox = getScreenBBox()
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
}
}
function directionNorthWest() {
var bbox = getScreenBBox()
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function directionNorthEast() {
var bbox = getScreenBBox()
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function directionSouthWest() {
var bbox = getScreenBBox()
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function directionSouthEast() {
var bbox = getScreenBBox()
return {
top: bbox.se.y,
left: bbox.se.x
}
}
function initNode() {
var div = d3Selection.select(document.createElement('div'))
div
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box')
return div.node()
}
function getSVGNode(element) {
var svgNode = element.node()
if (!svgNode) return null
if (svgNode.tagName.toLowerCase() === 'svg') return svgNode
return svgNode.ownerSVGElement
}
function getNodeEl() {
if (node == null) {
node = initNode()
// re-add node to DOM
document.body.appendChild(node)
}
return d3Selection.select(node)
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast),
// nw(northwest), sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox() {
var targetel = target || d3Selection.event.target
while (targetel.getScreenCTM == null && targetel.parentNode == null) {
targetel = targetel.parentNode
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
// Private - replace D3JS 3.X d3.functor() function
function functor(v) {
return typeof v === 'function' ? v : function() {
return v
}
}
return tip
}
// eslint-disable-next-line semi
}));
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
}
h1, a {
color: darkturquoise;
}
.legend {
font-size: 12px;
}
.sea {
fill: black;
}
.land {
fill: #999999;
opacity: .5;
}
.boundary {
fill: none;
stroke: lightgrey;
stroke-linejoin: round;
stroke-linecap: round;
}
circle {
opacity: 0.5;
fill: turquoise;
stroke: darkturquoise;
}
circle:hover {
opacity: 0.8;
}
/* Tooltip CSS */
.d3-tip {
line-height: 1.5;
font-weight: 400;
font-family: sans-serif;
padding: 6px;
background: rgba(0, 0, 0, 0.9);
color: #FFA500;
border-color: turquoise;
border-width: turquoise;
border-radius: 1px;
pointer-events: none;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 8px;
width: 100%;
line-height: 1.5;
color: rgba(0, 0, 0, 0.6);
position: absolute;
pointer-events: none;
}
/* Northward tooltips */
.d3-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}
/* Eastward tooltips */
.d3-tip.e:after {
content: "\25C0";
margin: -4px 0 0 0;
top: 50%;
left: -8px;
}
/* Southward tooltips */
.d3-tip.s:after {
content: "\25B2";
margin: 0 0 1px 0;
top: -8px;
left: 0;
text-align: center;
}
/* Westward tooltips */
.d3-tip.w:after {
content: "\25B6";
margin: -4px 0 0 -1px;
top: 50%;
left: 100%;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="d3-tip.js"></script>
<body>
<h1>One Laptop Per Child (#D3makeover)</h1>
<p>The circles represent the number of laptops from the One Laptop Per Child (OLPC) project are present in each country, per capita.</p>
<div id="map"></div>
<p>Legend (laptops per capita):</p>
<div id="legend"></div>
<p>Source: <a href="http://one.laptop.org/map">One Laptop Per Child</a></p>
<script>
const format = d3.format(',');
const tip = d3.tip()
.attr('class', 'd3-tip')
.offset([0, -10])
.html(function(d){
return "<strong>Country: </strong><span class='details'>" + d.country
+ "<br></span><strong>Laptop per capita: </strong><span class='details'>" + roundNumber(d.percapita) + "</span>"
+ "<br></span><strong>Total laptops: </strong><span class='details'>" + format(d.count) + "</span>";
});
var width = 900,
height = 500;
var projection = d3.geoEquirectangular()
.translate([width / 2, height / 2])
.scale(width / 2 / Math.PI);
var zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([[0,0],[width,height]])
.on("zoom", zoomed);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
svg.call(tip);
var g = svg.append("g").attr("class","map").call(zoom);
g.append("rect")
.attr("class", "sea")
.attr("x",0)
.attr("y",0)
.attr("width",width)
.attr("height",height);
d3.json("world-50m.json", function(error, world) {
g.append("path")
.datum(topojson.feature(world, world.objects.countries))
.attr("class", "land")
.attr("d", path);
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
d3.csv("olpc.csv", convertTextToNumbers, function(error, data) {
data.sort(function (a,b) {return d3.descending(a.percapita, b.percapita); });
var radius = d3.scaleSqrt()
.domain([0,d3.max(data, function(d){ return d.percapita; })])
.range([5,30]);
var circles = g.append("g").attr("class","circles");
circles.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d){
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function(d){
return projection([d.longitude, d.latitude])[1];
})
.attr("r", function(d){ return radius(d.percapita); })
.on('mouseover', function(d){
tip.direction('w').show(d);
d3.select(this).style('stroke-width', 3);
})
.on('mouseout', function(d){
tip.hide(d);
d3.select(this).style('stroke-width',0.3);
});
var legendWidth = 400;
var legendHeight = 75;
var legendData = [1,10,50,100,200];
var legendDataLength = legendData.length;
var legendOffset = 10;
var legend = d3.select("#legend").append("svg")
.attr("width", legendWidth)
.attr("height", legendHeight);
var legendCircles = legend.selectAll("circle")
.data(legendData)
.enter()
.append("g")
.attr("transform", function(d,i) {
let j = 0;
var x = (legendOffset * (1 + i));
for (j; j < i; j++ ){
x = x + (2 * radius(legendData[j]));
};
return "translate(" + x + "," + (legendHeight/2) + ")";
});
legendCircles.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", function(d){ return radius(d); });
legendCircles.append("text")
.text(function(d){return d;})
.attr("class", "legend")
.attr("x", 0)
.attr("y", "0.35em")
.style("text-anchor", "middle")
.style("fill", "#000")
});
});
function zoomed() {
var transform = d3.zoomTransform(this);
g.attr("transform", "translate(" + transform.x + "," + transform.y + ") scale(" + transform.k + ")");
};
function roundNumber(n){
const precision = 1000;
var roundedNumber = Math.round(n*precision);
return roundedNumber/precision;
};
function convertTextToNumbers(d) {
d.count = +d.count;
d.pop = +d.pop;
d.percapita = +d.percapita;
return d;
};
</script>
country count latitude longitude pop percapita
Uruguay 795500 -32.522779 -55.765835 3431555 231.8191024
Peru 980000 -9.189967 -75.015152 31376670 31.2333973
Rwanda 280313 -1.940278 29.873888 11609666 24.14479452
Federated States of Micronesia 800 7.425554 150.550812 104460 7.65843385
Honduras 57200 15.199999 -86.241905 8075060 7.083538698
Nicaragua 42000 12.865416 -85.207229 6082032 6.905586817
Mongolia 14500 46.862496 103.846656 2959134 4.900082254
Gabon 8100 -0.803689 11.609444 1725292 4.69485745
Gaza 6000 31.354676 34.308825 1.82E+06 3.303964758
Australia 61668 -25.274398 133.775136 23968973 2.572826128
West Bank 3600 31.952162 35.233154 1.72E+06 2.099125364
Paraguay 13549 -23.442503 -58.443832 6639123 2.040781591
Argentina 65500 -38.416097 -63.616672 43416755 1.508634167
Haiti 15000 18.971187 -72.285215 10711067 1.400420705
Fiji 1130 -16.578193 179.414413 892145 1.266610248
Costa Rica 5000 9.748917 -83.753428 4807850 1.039965889
Colombia 25316 4.570868 -74.297333 48228704 0.524915619
Solomon Islands 300 -9.64571 160.156194 583591 0.514058647
Mexico 58740 23.634501 -102.552784 127017224 0.462456966
Swaziland 500 -26.522503 31.465866 1286970 0.388509445
Mali 6135 17.570692 -3.996166 17599694 0.348585606
Papua New Guinea 2350 -6.314993 143.95555 7619321 0.308426433
Nepal 7680 28.394857 84.124008 28513700 0.26934421
Iraq 9150 33.223191 43.679291 36423395 0.251212167
Mozambique 6000 -18.665695 35.529562 27977863 0.214455264
Afghanistan 6165 33.93911 67.709953 32526562 0.1895374
Sri Lanka 1800 7.873054 80.771797 20715010 0.086893513
Greece 880 39.074208 21.824312 10954617 0.080331426
Lebanon 450 33.854721 35.862285 5850743 0.076913308
Dominican Republic 750 18.735693 -70.162651 10528391 0.071235956
Ethiopia 6840 9.145 40.489673 99390750 0.068819281
Cambodia 1000 12.565679 104.990963 15577899 0.064193509
Togo 310 8.619543 0.824782 7304578 0.042439139
Nigeria 6100 9.081999 8.675277 182201962 0.033479332
El Salvador 200 13.794185 -88.89653 6126583 0.032644624
Uganda 1200 1.373333 32.290275 39032383 0.030743703
Italy 1500 41.87194 12.56738 59797685 0.025084583
Philippines 2360 12.879721 121.774017 100699395 0.023436089
Thailand 1000 15.870032 100.992541 67959359 0.014714677
Guatemala 217 15.783471 -90.230759 16342897 0.01327794
Brazil 2600 -14.235004 -51.92528 207847528 0.01250917
Ghana 300 7.946527 -1.023194 27409893 0.010944953
Kenya 500 -0.023559 37.906193 46050302 0.010857692
Pakistan 1200 30.375321 69.345116 188924874 0.006351731
Iran 343 32.427908 53.688046 79109272 0.004335775
Malaysia 100 4.210484 101.975766 30331007 0.003296956
India 1000 20.593684 78.96288 1311050527 0.000762747
China 1000 35.86166 104.195397 1376048943 0.000726718
Indonesia 100 -0.789275 113.921327 257563815 0.000388253
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment