This is a visualization of money raised to sponsor hospital trips via plane for remote communities in Papua New Guinea.
For every $150, the children would be able to pay for a plane to fly on one leg of a trip.
The plane was based in one city, the people were in another city, and the hospital was in a third.
The visualization randomly selects a large city on the map and plans a route for the plane to make it to
that city. It then shows an animation of a plane flying and the count of total full legs of the trip that were sponsored.
Enter a number at the bottom for the dollar amount raised. Push "go". The plane flys until it runs out of gas and lands at that point to show lack of completion and how much more needs to be raised.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.count {
font-size: 100px;
pointer-events: none;
}
.subunit.PNB { fill: #cdc; }
.subunit.PNX { fill: #dcd; }
.subunit.IDN { display: none; }
svg { fill: rgb(216,216,216); }
.subunit-boundary {
fill: none;
stroke: #777;
stroke-dasharray: 2,2;
stroke-linejoin: round;
}
.subunit-boundary.IDN {
stroke: #aaa;
}
.place,
.place-label {
fill: #444;
}
text {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 10px;
pointer-events: none;
}
.route {
fill: none;
stroke: red;
stroke-dasharray: 2,2;
stroke-linejoin: round;
stroke-width: 1em;
}
.plane.boy {
fill: #2b65EC;
stroke-width: 0;
}
.plane.girl {
fill: pink;
stroke-width: 0;
}
.plane.both {
fill: #fff;
stroke: #000;
stroke-width: 0.1 em;
}
</style>
<body>
<script src="browser-polyfill.js"></script>
<script src="itertools.min.js"></script>
<script src="jquery.min.js"></script>
<script src="d3.min.js" charset="utf-8"></script>
<script src="topojson.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Faster+One|Frijole" rel="stylesheet">
<script>
function shuffle(array) {
counter = array.length;
// While there are elements in the array
while (counter > 0) {
// Pick a random index
index = Math.floor(Math.random() * counter);
// Decrease counter by 1
counter--;
// And swap the last element with it
temp = array[counter];
array[counter] = array[index];
array[index] = temp;
}
return array;
}
var width = 960,
height = 450;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.mercator()
.center([148.5, -6.5])
.scale(3000)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var airportMap = {};
function transition(plane, route, percent) {
var l = route.node().getTotalLength();
plane.transition()
.duration(4000)
.attrTween("transform", delta(plane, route.node(), percent))
.remove();
return l*10;
}
function delta(plane, path, percent) {
var l = path.getTotalLength();
var plane = plane;
return function(i) {
return function(t) {
var s = 5*Math.min(Math.sin(Math.PI * t) * 0.7, 0.3);
if( t > percent) {
var p = path.getPointAtLength(percent * l);
var t2 = Math.min(t + 0.05, 1);
var p2 = path.getPointAtLength(t2 * l);
var x = p2.x - p.x;
var y = p2.y - p.y;
var r = 90 - Math.atan2(-y, x) * 180 / Math.PI + (t - percent)*360;
} else {
var p = path.getPointAtLength(t * l);
var t2 = Math.min(t + 0.05, 1);
var p2 = path.getPointAtLength(t2 * l);
var x = p2.x - p.x;
var y = p2.y - p.y;
var r = 90 - Math.atan2(-y, x) * 180 / Math.PI;
}
return "translate(" + p.x + "," + p.y + ") scale(" + s + ") rotate(" + r + ")";
}
}
}
function fly(origin, destination, gender, percent) {
var route = svg.append("path")
.datum({type: "LineString", coordinates: [airportMap[origin], airportMap[destination]]})
.attr("class", "route")
.attr("d", path);
var plane = svg.append("path")
.attr("class", "plane " + gender)
.attr("d", "m25.21488,3.93375c-0.44355,0 -0.84275,0.18332 -1.17933,0.51592c-0.33397,0.33267 -0.61055,0.80884 -0.84275,1.40377c-0.45922,1.18911 -0.74362,2.85964 -0.89755,4.86085c-0.15655,1.99729 -0.18263,4.32223 -0.11741,6.81118c-5.51835,2.26427 -16.7116,6.93857 -17.60916,7.98223c-1.19759,1.38937 -0.81143,2.98095 -0.32874,4.03902l18.39971,-3.74549c0.38616,4.88048 0.94192,9.7138 1.42461,13.50099c-1.80032,0.52703 -5.1609,1.56679 -5.85232,2.21255c-0.95496,0.88711 -0.95496,3.75718 -0.95496,3.75718l7.53,-0.61316c0.17743,1.23545 0.28701,1.95767 0.28701,1.95767l0.01304,0.06557l0.06002,0l0.13829,0l0.0574,0l0.01043,-0.06557c0,0 0.11218,-0.72222 0.28961,-1.95767l7.53164,0.61316c0,0 0,-2.87006 -0.95496,-3.75718c-0.69044,-0.64577 -4.05363,-1.68813 -5.85133,-2.21516c0.48009,-3.77545 1.03061,-8.58921 1.42198,-13.45404l18.18207,3.70115c0.48009,-1.05806 0.86881,-2.64965 -0.32617,-4.03902c-0.88969,-1.03062 -11.81147,-5.60054 -17.39409,-7.89352c0.06524,-2.52287 0.04175,-4.88024 -0.1148,-6.89989l0,-0.00476c-0.15655,-1.99844 -0.44094,-3.6683 -0.90277,-4.8561c-0.22699,-0.59493 -0.50356,-1.07111 -0.83754,-1.40377c-0.33658,-0.3326 -0.73578,-0.51592 -1.18194,-0.51592l0,0l-0.00001,0l0,0z");
return transition(plane, route, percent);
}
var randomColor = (function(){
var golden_ratio_conjugate = 0.618033988749895;
var h = Math.random();
var hslToRgb = function (h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return '#'+Math.round(r * 255).toString(16)+Math.round(g * 255).toString(16)+Math.round(b * 255).toString(16);
};
return function(){
h += golden_ratio_conjugate;
h %= 1;
return hslToRgb(h, 0.5, 0.60);
};
})();
d3.json("png.json", function(error, papua) {
if (error) return console.error(error);
var subunits = topojson.feature(papua, papua.objects.subunits);
svg.append("path")
.datum(subunits)
.attr("d", path);
svg.selectAll(".subunit")
.data(topojson.feature(papua, papua.objects.subunits).features)
.enter().append("path")
.attr("class", function(d) { return "subunit " +d.properties.adm0_a3 + " " + d.id; })
.attr("d", path)
.style({fill: randomColor});
svg.append("path")
.datum(topojson.mesh(papua, papua.objects.subunits, function(a, b) { return a !== b && a.properties.adm0_a3 !== "IDN"; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(papua, papua.objects.subunits, function(a, b) { return a === b && a.properties.adm0_a3 === "IDN"; }))
.attr("d", path)
.attr("class", "subunit-boundary IDN");
svg.append("path")
.datum(topojson.feature(papua, papua.objects.places))
.attr("d", path)
.attr("class", "place");
svg.selectAll(".place-label")
.data(topojson.feature(papua, papua.objects.places).features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("dy", ".15em")
.text(function(d) { return d.properties.NAME; });
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start" : "end"; });
count = svg.append("text")
.attr("class", "count")
.attr("x", "450")
.attr("y", "130")
.style({fill: randomColor,
"font-family": 'Faster One, cursive'})
.text("0")
var geos = topojson.feature(papua, papua.objects.places).features;
for (i in geos) {
airportMap[geos[i].properties.NAME] = geos[i].geometry.coordinates;
}
dests = topojson.feature(papua, papua.objects.places).features.map(function(d){return d.properties.NAME});
});
function handleClick(event){
//cityGen = itertools.permutations(topojson.feature(papua, papua.objects.places).features.map(function(d){return d.properties.NAME}),2);
//cityArray = shuffle(Array.from(cityGen)).slice(0,5);
var costOfOneLeg = 150;
var money = + document.getElementById("money").value;
var gender = $('input[name="gender"]:checked').val();
dests.splice(dests.indexOf("Port Moresby"), 1);
dests.splice(dests.indexOf("Wewak"), 1);
dests.splice(dests.indexOf("Mendi"), 1);
cityArray = [];
newDest = "Mendi";
cityArray.push(["Port Moresby",newDest]);
cityArray.push([newDest,"Wewak"]);
cityArray.push(["Wewak", "Port Moresby"]);
while( cityArray.length < money/costOfOneLeg ) {
newDest = dests[Math.floor(Math.random()*dests.length)];
dests.splice(dests.indexOf(newDest), 1);
cityArray.push(["Port Moresby",newDest]);
cityArray.push([newDest,"Wewak"]);
cityArray.push(["Wewak", "Port Moresby"]);
}
var i = 0;
fly(cityArray[i][0], cityArray[i][1], gender, money/costOfOneLeg - i );
i++;
intID = setInterval(function(){
if (i <= money/costOfOneLeg) {
count.text(i)
.style({fill: randomColor})
fly(cityArray[i][0], cityArray[i][1], gender, money/costOfOneLeg - i );
i++;
} else {
clearInterval(intID)
}
}, 2000);
return false;
}
/* JavaScript goes here. */
</script>
<br>
<input name="Submit" type="submit" value="GO!" onclick="return handleClick()">
<input type="text" id="money" placeholder="Money Raised…">
<input type="radio" name="gender" value="boy"> Boys
<input type="radio" name="gender" value="girl"> Girls
<input type="radio" name="gender" value="both"> Together!