Skip to content

Instantly share code, notes, and snippets.

@hotzeplotz
Last active October 13, 2015 12:14
Show Gist options
  • Select an option

  • Save hotzeplotz/8f64b9a8bd98042a712c to your computer and use it in GitHub Desktop.

Select an option

Save hotzeplotz/8f64b9a8bd98042a712c to your computer and use it in GitHub Desktop.
Future of large cities - GDP
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Future of large cities - GDP</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox.js/v2.1.4/mapbox.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox.js/v2.1.4/mapbox.css' rel='stylesheet' />
<link href='style.css' rel='stylesheet' />
</head>
<body>
<!-- This example requires d3 for AJAX and the brush,
though you can bring your own library. -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js' charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='map' class='dark'></div>
<div class="metro-tooltip"></div>
<div id='brush' class='ui-brush'></div>
<script src='make-map.js'></script>
<script src='leaflet-hash.js'></script>
<script>
make_map({
data_file: 'largecities.json',
map_id: 'map',
initial_position: [0,0],
initial_zoom: 2,
brush: false,
time_var: 'gdp_pcapit',
growth_var: 'gdp_growth',
cumulative_var: 'gdp_2012',
time_var_label: 'GDP Per capita (2012)',
time_var_unit: 'US$',
time_var_extent: [0, 72600074.2624515], // use actual gloabl GDP figure used for newspaper calculations
time_var_value_function: function(val) {
return Math.round(val * 1000);
},
growth_var_label: 'Average annual GDP growth forecast 2012&mdash;2030',
growth_var_unit: '%'
});
</script>
</body>
</html>
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.
(function(window) {
var HAS_HASHCHANGE = (function() {
var doc_mode = window.documentMode;
return ('onhashchange' in window) &&
(doc_mode === undefined || doc_mode > 7);
})();
L.Hash = function(map) {
this.onHashChange = L.Util.bind(this.onHashChange, this);
if (map) {
this.init(map);
}
};
L.Hash.parseHash = function(hash) {
if(hash.indexOf('#') === 0) {
hash = hash.substr(1);
}
var args = hash.split("/");
if (args.length == 3) {
var zoom = parseInt(args[0], 10),
lat = parseFloat(args[1]),
lon = parseFloat(args[2]);
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
return false;
} else {
return {
center: new L.LatLng(lat, lon),
zoom: zoom
};
}
} else {
return false;
}
};
L.Hash.formatHash = function(map) {
var center = map.getCenter(),
zoom = map.getZoom(),
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
return "#" + [zoom,
center.lat.toFixed(precision),
center.lng.toFixed(precision)
].join("/");
},
L.Hash.prototype = {
map: null,
lastHash: null,
parseHash: L.Hash.parseHash,
formatHash: L.Hash.formatHash,
init: function(map) {
this.map = map;
// reset the hash
this.lastHash = null;
this.onHashChange();
if (!this.isListening) {
this.startListening();
}
},
removeFrom: function(map) {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (this.isListening) {
this.stopListening();
}
this.map = null;
},
onMapMove: function() {
// bail if we're moving the map (updating from a hash),
// or if the map is not yet loaded
if (this.movingMap || !this.map._loaded) {
return false;
}
var hash = this.formatHash(this.map);
if (this.lastHash != hash) {
location.replace(hash);
this.lastHash = hash;
}
},
movingMap: false,
update: function() {
var hash = location.hash;
if (hash === this.lastHash) {
return;
}
var parsed = this.parseHash(hash);
if (parsed) {
this.movingMap = true;
this.map.setView(parsed.center, parsed.zoom);
this.movingMap = false;
} else {
this.onMapMove(this.map);
}
},
// defer hash change updates every 100ms
changeDefer: 100,
changeTimeout: null,
onHashChange: function() {
// throttle calls to update() so that they only happen every
// `changeDefer` ms
if (!this.changeTimeout) {
var that = this;
this.changeTimeout = setTimeout(function() {
that.update();
that.changeTimeout = null;
}, this.changeDefer);
}
},
isListening: false,
hashChangeInterval: null,
startListening: function() {
this.map.on("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
this.hashChangeInterval = setInterval(this.onHashChange, 50);
}
this.isListening = true;
},
stopListening: function() {
this.map.off("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
}
this.isListening = false;
}
};
L.hash = function(map) {
return new L.Hash(map);
};
L.Map.prototype.addHash = function() {
this._hash = L.hash(this);
};
L.Map.prototype.removeHash = function() {
this._hash.removeFrom();
};
})(window);
function make_map(map_data) {
L.mapbox.accessToken = 'pk.eyJ1IjoibHNlY2l0aWVzIiwiYSI6IjJ6ZjA5UGMifQ.8y1yiWucAic55wg5YoiTGQ';
var map = L.mapbox.map(map_data.map_id, 'lsecities.nmkn5fcj', {
attributionControl: false,
infoControl: true
})
.setView(map_data.initial_position, map_data.initial_zoom),
$metro_tooltip = $('.metro-tooltip');
var hash = new L.Hash(map);
// Credit our friendly open data producers.
/*map.infoControl.addInfo (
'<a href="http://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php">Data from USGS</a>');
*/
function enterLayer(){
var metroName = this.feature.properties.city_name.length > 0 ? this.feature.properties.city_name : this.feature.properties.city_name_alt,
val_2012 = map_data.time_var_value_function(this.feature.properties[map_data.time_var]),
val_growth = Math.round(this.feature.properties[map_data.growth_var] * 100) / 100;
$metro_tooltip.html(
'<h3>' + metroName + '</h3>' +
'<dl>' +
'<dt>' + map_data.time_var_label + '</dt>' +
'<dd>' + val_2012 + ' ' + map_data.time_var_unit + '</dd>' +
'<dt>' + map_data.growth_var_label + '</dt>' +
'<dd>' + val_growth + map_data.growth_var_unit + '</dd>' +
'</dl>'
).show();
this.bringToFront();
this.setStyle({
weight:2,
opacity: 1
});
}
function leaveLayer(){
$metro_tooltip.hide();
this.bringToBack();
this.setStyle({
weight:1,
opacity:.5
});
}
// Create a new layer with a special pointToLayer function
// that'll generate scaled points.
var citiesLayer = L.geoJson(null, {
pointToLayer: scaledPoint,
onEachFeature: function(feature, layer) {
layer.on({
mouseover: enterLayer,
mouseout: leaveLayer
});
}
})
.addTo(map);
var fill; // attach function within d3.json();
map.on('zoomend', function (e) {
zoomHandler();
});
function zoomHandler() {
citiesLayer.setStyle(citiesSize);
}
function citiesSize(feature) {
var currentZoom = map.getZoom();
return { radius: pointRadius(feature)};
}
function pointColor(feature) {
return fill(feature.properties[map_data.growth_var]);
}
function pointRadius(feature) {
return Math.sqrt(Math.sqrt(Math.round((feature.properties[map_data.time_var]) * 100) / 100) * map.getZoom());
}
function rectWidth(width, d, index) {
var x = d3.scale.linear()
.range([0, width])
.domain(map_data.time_var_extent);
return x(d.group_value);
}
function scaledPoint(feature, latlng) {
var val_2012 = Math.round((feature.properties[map_data.time_var] * map_data.time_var_multiplier) * 100) / 100,
val_gr = Math.round(feature.properties[map_data.growth_var] * 100) / 100;
return L.circleMarker(latlng, {
radius: pointRadius(feature),
fillColor: pointColor(feature),
fillOpacity: 0.7,
weight: 0.5,
color: '#fff'
});
}
// Request our data and add it to the earthquakesLayer.
d3.json(map_data.data_file, function(err, data) {
var min = d3.min(data.features.map(function(d) { return d.properties[map_data.growth_var]; })),
max = d3.max(data.features.map(function(d) { return d.properties[map_data.growth_var]; }));
/**
* add value of cumulative variable so far to each feature's properties
* sort by cumulative variable first or things won't make sense
*/
var val_so_far = 0; // keep track of pop so far in map below
var sorted_data_features = data.features.sort(function(a,b) { return b.properties[map_data.cumulative_var] - a.properties[map_data.cumulative_var]; }); // sort descending
var sorted_data_features_with_val_so_far = sorted_data_features.map(function(currentValue, index, array) {
currentValue.properties.val_so_far = +val_so_far;
val_so_far = +(currentValue.properties.val_so_far) + +(currentValue.properties[map_data.cumulative_var]);
return currentValue;
});
// once done, replace original features with features including the new variable with cumulative values
data.features = sorted_data_features_with_val_so_far;
/**
* Calculate group values for first 1, 5, 10, 20, 50, 100, 500 and 700 cities as per newspaper spread
*/
var group_data = [ 1, 5, 10, 20, 50, 100, 500, 700 ].map(function(d, i, array) {
var previous_groups_cumulative_total = (i > 0) ?
data.features[array[i - 1]].properties.val_so_far : 0;
return {
city_count: d,
group_value: data.features[d].properties.val_so_far - previous_groups_cumulative_total,
previous_groups_cumulative_total: previous_groups_cumulative_total
};
});
fill = d3.scale.threshold()
.domain([ -1 , 0, 1, 2, 3, 4, 5 ])
.range([ "#b3c8eb", "#d3dff4", "#f8d7db", "#eea9b5", "#e48391", "#de5f74", "#d73f5b", "#d22247"]);
citiesLayer.addData(data);
if(true == map_data.brush) {
setBrush(data, group_data);
}
});
function setBrush(data, group_data) {
var container = d3.select('#brush'),
margin = {top: 20, right: 20, bottom: 20, left: 20},
width = container.node().offsetWidth - margin.left - margin.right,
height = 80;
var val_extent = map_data.time_var_extent; // todo: use actual world population figure from printed version
var svg = container.append('svg')
.attr('width', width )
.attr('height', height + margin.top + margin.bottom);
var context = svg.append('g')
.attr('class', 'context')
.attr('transform', 'translate(' +
margin.left + ',' +
margin.top + ')');
var x = d3.scale.linear()
.range([0, width])
.domain(val_extent);
var xPercent = d3.scale.linear()
.range([0, width])
.domain([0,1]);
var xAxis = d3.svg.axis()
.scale(xPercent)
.tickFormat(d3.format('%')).orient('bottom');
var xAxisGroup = context.append("g")
.attr('transform', 'translate(0, ' + (height) + ')')
.attr('class', 'xAxis')
.call(xAxis);
var brush = d3.svg.brush()
.x(x)
.on('brushend', brushend);
context.selectAll('rect.city')
.data(group_data)
.enter()
.append('rect')
.attr('transform', function(d) {
return 'translate(' + [x(d.previous_groups_cumulative_total), 0] + ')';
})
.attr('width', rectWidth.bind(null, width))
.attr('height', height * 0.7)
.attr('data-cumulative-count', function(d, i) {
return d.city_count;
})
.attr('opacity', 1)
.attr('fill', function(d, i) {
var base_color = d3.rgb('#d22247');
return base_color.brighter(0.5 * i);
});
context.selectAll('text.city-count')
.data(group_data)
.enter()
.append('text')
.attr('class', 'city-count')
.attr('x', function(d, i) { return x(d.previous_groups_cumulative_total); })
.attr('y', '-2')
.text(function(d, i) { return d.city_count; });
context.append('g')
.attr('class', 'x brush')
.call(brush)
.selectAll('rect')
.attr('y', -6)
.attr('height', height);
function brushend() {
var filter;
// If the user has selected no brush area, share everything.
if (brush.empty()) {
filter = function() { return true; }
} else {
// Otherwise, restrict features to only things in the brush extent.
filter = function(feature) {
return feature.properties.val_so_far > +brush.extent()[0] &&
((+feature.properties.val_so_far) + (+feature.properties[map_data.cumulative_var])) < (+brush.extent()[1]);
};
}
var filtered = data.features.filter(filter);
citiesLayer.clearLayers()
.addData(filtered);
}
}
}
body {
margin:0;
padding:0;
font-family: sans-serif;
}
#map {
position:absolute;
top:0;
bottom:0;
width:100%;
}
.metro-tooltip{
position: absolute;
top: 2em;
right: 1em;
z-index: 6;
background: rgba(0,0,0,.75);
color: white;
padding: .5em .75em;
font-size: .85em;
display: none;
}
.xAxis path,
.xAxis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
.ui-brush {
background: #f8f8f8;
position: absolute;
bottom: 0;
right: 0;
left: 0;
height: 140px;
display: none;
}
.brush .extent {
stroke: #fff;
fill-opacity: 0.125;
shape-rendering: crispEdges;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment