Building on code originally used for this blog post, this example shows an integrated and animated map + timeseries graph. It uses the Nordpil world cities dataset (based on data from the UN Population Division), and shows the growth in metropolitan area popluations from 1950 to 2005. The data is filtered to show only metro areas with 2005 populations greater than 2.5 million, and note that the scale is such that the Tokyo metro area is allowed to grow off the chart in order to keep a linear scale with reasonable resolution.
Click on a dot on the map to view its history. Click on the animation button or move the date slider to view the temporal changes on the map.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.land {
fill: #bbb;
}
.boundary {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
}
.handleText, .siteNameText, .titleText, .axis {
opacity: 0.5;
font-size: 10px;
}
.mapPoint, .linegraph {
stroke: black;
stroke-width: 0.5px;
fill-opacity: 0.8;
}
.mapPoint {
cursor: pointer;
}
.linegraph_line {
stroke: #bbb;
fill: none;
stroke-width: 1px;
}
.startIcon {
opacity: 0.5;
cursor: pointer;
}
.axis .domain, .slider .handle {
stroke: #555;
stroke-opacity: .5;
stroke-linecap: round;
}
.axis .domain {
stroke-width: 1.25px;
}
.slider .handle {
stroke-width: 2.5px;
cursor: move;
}
.axis {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.axis .domain {
fill: none;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 500,
mapWidth = 600,
mapHeight = 320;
var leftMargin = Math.max(0,(width-mapWidth) / 2);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", "translate(" + leftMargin + ",0)")
.append("svg")
.attr("id", "map")
.attr("width", mapWidth)
.attr("height", mapHeight);
d3.select("#map").append("g")
.attr("id","mapCountries");
d3.select("#map").append("g")
.attr("id","mapCities");
svg.append("g")
.attr("id","linegraphs");
var projection = d3.geo.mercator()
.center([0,-30])
.scale(90)
.rotate([0,0])
.translate([280,250]);
var geo_path = d3.geo.path()
.projection(projection);
var x = d3.scale.linear()
.domain([1950, 2005])
.range([leftMargin + 20, leftMargin + 550])
.clamp(true);
var y = d3.scale.linear()
.domain([0, 20000])
.range([475, 380]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top")
.tickFormat(function(d) { if (d == 1950 || d == 2005) {return d}; })
.tickSize(0)
.tickPadding(12);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right")
.tickValues([0, 4000, 8000, 12000, 16000, 20000])
.tickFormat(function(d) { return d / 1000 })
.tickSize(0)
.tickPadding(12);
var line = d3.svg.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); })
.interpolate("linear");
var color = d3.scale.linear()
.domain([0,2000,4000,6000,8000,10000,12000,14000,16000,18000,20000])
.range(["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"])
.interpolate(d3.interpolateHcl);
var legendColorList = [];
for (var i = 0; i <= 20000; i+=100) { legendColorList.push(i); }
var brush = d3.svg.brush()
.x(x)
.extent([0, 0]);
var startTriangle = "M0,0L0,10L8,5",
stopRectangle = "M0,0L0,10L10,10L10,0";
// country boundaries displayed differently in chrome and firefox
var country_boundary_width = "0.5px";
if (!!window.chrome && !!window.chrome.webstore) {
country_boundary_width = "0.1px";
}
var urbanareas_years = [];
for (var i=1950; i<2010; i=i+5){ urbanareas_years.push(i) }
var initialSite = "London_United Kingdom",
sliderlocation = 1950.0,
animationOn = false,
animationObj = {};
d3.json("world-50m.json", function(error, topology) {
if (error) throw error;
d3.tsv("urbanareas1_1.tsv", function(error, urbanareas_all) {
if (error) throw error;
// filter, sort and format urban areas data
var urbanareas = urbanareas_all.filter(function(d){ return d['pop2005'] > 2500; })
urbanareas.sort(function(a,b){ return a['pop2005'] - b['pop2005']; })
urbanareas.forEach(function(d){
if (d['City_Alternate'] == "") {
d['City_Alternate'] = d['City'];
}
d['ID'] = d['City_Alternate'] + "_" + d['Country'];
d['lat'] = +d['Latitude'];
d['lon'] = +d['Longitude'];
d['history'] = [];
urbanareas_years.forEach(function(yr){
d['history'].push(+d['pop' + yr])
})
})
// draw axes, slider handle, and labels
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 358 + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (leftMargin + 575) + ",0)")
.call(yAxis);
d3.select("#linegraphs").selectAll(".colorLegend")
.data(legendColorList)
.enter().append("circle")
.attr("class", "colorLegend")
.attr("r", 2.5)
.attr("fill", function(d) { return color(d); })
.attr("cy", function(d){ return y(d); })
.attr("cx", function(d){ return leftMargin + 575; });
svg.append("text")
.attr("class", "siteNameText")
.attr("id", "siteText" )
.attr("x", leftMargin + 560)
.attr("y", 325)
.attr("text-anchor", "end")
.text(initialSite.replace("_",", "));
svg.append("text")
.attr("class", "titleText")
.attr("id", "tempTypeText1" )
.attr("x", leftMargin + 10)
.attr("y", 325)
.attr("text-anchor", "start")
.text("METRO AREA POPULATION (millions)");
svg.append("text")
.attr("class", "siteNameText")
.attr("x", leftMargin + 290)
.attr("y", 346)
.attr("text-anchor", "end")
.text("animate");
svg.append("path")
.attr("class", "startIcon")
.attr("d", startTriangle)
.attr("transform", "translate(" + (leftMargin + 300) + "," + 338 + ")");
var slider = svg.append("g")
.attr("class", "slider");
slider.selectAll(".extent,.resize")
.remove();
var handle = slider.append("line")
.attr("class", "handle")
.attr("x1", leftMargin + 200)
.attr("y1", 358)
.attr("x2", leftMargin + 200)
.attr("y2", 495);
var handleText = slider.append("text")
.attr("class", "handleText")
.attr("x", leftMargin + 210)
.attr("y", 495)
.attr("text-anchor", "start")
.text("");
// draw map and cities
d3.select("#mapCountries").append("path")
.datum(topojson.feature(topology, topology.objects.land))
.attr("d", geo_path)
.attr("class", "land");
svg.select("#mapCountries").append("path")
.datum(topojson.mesh(topology,
topology.objects.countries,
function(a, b) {
return a !== b && (a.id / 1000 | 0) === (b.id / 1000 | 0);
}))
.attr("d", geo_path)
.attr("stroke-width", country_boundary_width)
.attr("class", "boundary");
d3.select("#mapCities").selectAll(".mapPoint")
.data(urbanareas)
.enter()
.append("circle")
.attr("class", "mapPoint")
.attr("r", 2.5)
.style("stroke-width", function(d){ if (d.ID == initialSite) { return 1.5 } else { return 0.5 }})
.attr("city_ID", function(d) {return d.ID;})
.attr("station_name", function(d) {return d.City_Alternate;})
.attr("country_name", function(d) {return d.Country;})
.attr("transform", function(d) {return "translate(" + projection([d.lon,d.lat]) + ")";});
// draw timeseries graph
var linedata = urbanareas.filter(function(k){ return k.ID == initialSite; })[0].history;
d3.select("#linegraphs").selectAll(".linegraph_line")
.data([d3.zip(urbanareas_years,linedata)])
.enter().append("path")
.attr("class", "linegraph_line")
.attr("d", function(d){ return line(d);});
d3.select("#linegraphs").selectAll(".linegraph")
.data(linedata)
.enter().append("circle")
.attr("class", "linegraph")
.attr("r", 2)
.attr("fill", function(d) { return color(d); })
.attr("cy", function(d){ return y(d); })
.attr("cx", function(d,i){ return x(urbanareas_years[i]); });
// interaction functions
function updateMapColors(year) {
d3.select("#mapCities").selectAll(".mapPoint")
.attr("fill", function(d) {
var i = urbanareas_years.indexOf(Math.floor(year / 5) * 5);
var val = d.history[i];
if (year > urbanareas_years[i]) {
val += (d.history[i+1] - d.history[i]) / (urbanareas_years[i+1] - year);
}
return color(val);
});
};
function updateLinegraph(city_ID) {
var linedata = urbanareas.filter(function(k){ return k.ID == city_ID; })[0].history;
d3.select("#linegraphs").selectAll(".linegraph_line")
.data([d3.zip(urbanareas_years,linedata)])
.transition()
.attr("d", function(d){ return line(d);});
d3.select("#linegraphs").selectAll(".linegraph")
.data(linedata)
.transition()
.attr("fill", function(d) { return color(d); })
.attr("cy", function(d){ return y(d); });
}
function brushed() {
var value = brush.extent()[0];
if (d3.event.sourceEvent) {
value = x.invert(d3.mouse(this)[0]);
brush.extent([value, value]);
}
var year = Math.floor(value);
handle
.attr("x1", x(value))
.attr("x2", x(value));
handleText
.attr("x", x(value) + 5)
.text(year);
sliderlocation = value;
updateMapColors(year);
};
function animator() {
slider
.call(brush.extent([sliderlocation, sliderlocation]))
.call(brush.event);
if (sliderlocation >= 2005.0) {
animationOn = false;
clearInterval(animationObj);
d3.select(".startIcon")
.attr("d", startTriangle);
} else {
sliderlocation += 1.0/4.0;
}
};
// apply interaction functions
brush.on("brush", brushed);
slider
.call(brush)
.call(brush.extent([sliderlocation, sliderlocation]))
.call(brush.event);
d3.select(".startIcon")
.on("click", function(e) {
if (animationOn) {
animationOn = false;
clearInterval(animationObj);
d3.select(this).attr("d", startTriangle);
} else {
animationOn = true;
d3.select(this).attr("d", stopRectangle);
animationObj = setInterval( animator, 1 );
}
});
d3.select("#mapCities").selectAll(".mapPoint")
.on("click", function(e){
pt = d3.select(this)[0][0].attributes;
d3.select("#siteText").text(pt.station_name.value + ", " + pt.country_name.value);
d3.selectAll(".mapPoint").style("stroke-width", 0.5);
d3.select(this).style("stroke-width", 1.5);
updateLinegraph(pt.city_ID.value);
});
});
});
</script>
https://d3js.org/d3.v3.min.js
https://d3js.org/topojson.v1.min.js