|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>D3: World Oil Consumption by Major Nations</title> |
|
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script> |
|
<style type="text/css"> |
|
/* CSS elements for the graph */ |
|
h1 { |
|
font-family: Helvetica, sans-serif; |
|
font-size: 18px; |
|
font-weight: bold; |
|
} |
|
|
|
h3 { |
|
font-family: Helvetica, sans-serif; |
|
font-size: 12px; |
|
} |
|
|
|
.area { |
|
stroke: none; |
|
cursor: pointer; |
|
} |
|
|
|
.area:hover { |
|
fill: rgb(175, 240, 91); |
|
} |
|
|
|
#backButton { |
|
cursor: pointer; |
|
} |
|
|
|
#backButton rect { |
|
fill: #3E50B4; |
|
} |
|
|
|
#backButton text { |
|
font-family: Helvetica, sans-serif; |
|
font-weight: bold; |
|
font-size: 14px; |
|
fill: white; |
|
} |
|
|
|
#backButton:hover rect { |
|
fill: rgb(26, 199, 194); |
|
} |
|
|
|
#backButton:hover text { |
|
fill: white; |
|
} |
|
|
|
.unclickable { |
|
pointer-events: none; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<h1>World Oil Consumption by major nations(OECD and Non OECD), between 1990 and 2016</h1> |
|
<h3>Unit: Mt</h3> |
|
<script type="text/javascript"> |
|
|
|
//Width and height |
|
var w = 1400; |
|
var h = 450; |
|
var padding = 20; |
|
|
|
//Tracks view state. |
|
var viewState = 0; |
|
|
|
//Tracks most recently viewed/clicked 'type'. Possible values: |
|
//"OECD" or "nonOECD" |
|
var viewType; |
|
|
|
var dataset, thisTypeDataset, xScale, yScale, xAxis, yAxis, area; |
|
|
|
//For converting strings to Dates |
|
var parseTime = d3.timeParse("%Y"); |
|
|
|
//For converting Dates to strings |
|
var formatTime = d3.timeFormat("%Y"); |
|
|
|
//Define key function, to be used when binding data |
|
var key = function(d) { |
|
return d.key; |
|
}; |
|
|
|
//Set up stack methods |
|
var nationStack = d3.stack(); |
|
var typeStack = d3.stack(); |
|
|
|
//Load in data |
|
d3.request("world_oil.csv") |
|
.mimeType("text/csv") |
|
.get(function(response) { |
|
|
|
//Parse each row of the CSV into an array of string values |
|
var rows = d3.csvParseRows(response.responseText); |
|
|
|
//Make dataset an empty array |
|
dataset = []; |
|
|
|
for (var i = 3; i < rows.length; i++) { |
|
|
|
dataset[i - 3] = { |
|
date: parseTime(rows[i][0]) //Make a new Date object for each year |
|
}; |
|
|
|
//Loop once for each nation in this row |
|
for (var j = 1; j < rows[i].length; j++) { |
|
|
|
var region = rows[0][j]; |
|
var nation = rows[1][j]; |
|
var regionNation = rows[0][j] + " " + rows[1][j]; |
|
var type = rows[2][j]; |
|
var consumption = rows[i][j]; |
|
|
|
//If consumption value exists |
|
if (consumption) { |
|
consumption = parseInt(consumption); |
|
} else { |
|
consumption = 0; |
|
} |
|
|
|
//Append a new object with type, nation, and consumption |
|
dataset[i - 3][regionNation] = { |
|
"region": region, |
|
"nation": nation, |
|
"type": type, |
|
"consumption": consumption |
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
//Make typeDataset an empty array for separate type of nations for annual total |
|
typeDataset = []; |
|
|
|
for (var i = 3; i < rows.length; i++) { |
|
|
|
//Create a new object |
|
typeDataset[i - 3] = { |
|
date: parseTime(rows[i][0]), //Make a new Date object for each year |
|
"OECD": 0, |
|
"nonOECD": 0, |
|
}; |
|
|
|
//Loop once for each nation in this row |
|
for (var j = 1; j < rows[i].length; j++) { |
|
|
|
var type = rows[2][j]; |
|
var consumption = rows[i][j]; |
|
|
|
//If consumption value exists |
|
if (consumption) { |
|
consumption = parseInt(consumption); //Convert from string to int |
|
} else { |
|
consumption = 0; |
|
} |
|
|
|
//Add consumption value to existing sum |
|
typeDataset[i - 3][type] += consumption; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
//Tell stack function where to find the keys |
|
var types = [ "OECD", "nonOECD" ]; |
|
typeStack.keys(types); |
|
|
|
//Stack the data and log it out |
|
var typeSeries = typeStack(typeDataset); |
|
|
|
|
|
//D3 CHART |
|
|
|
|
|
//Create scale functions |
|
xScale = d3.scaleTime() |
|
.domain([ |
|
d3.min(dataset, function(d) { return d.date; }), |
|
d3.max(dataset, function(d) { return d.date; }) |
|
]) |
|
.range([padding, w - padding * 2]); |
|
|
|
yScale = d3.scaleLinear() |
|
.domain([ |
|
0, |
|
d3.max(typeDataset, function(d) { |
|
var sum = 0; |
|
|
|
//Loops once for each row, to calculate the total (sum) of consumption of all nations |
|
for (var i = 0; i < types.length; i++) { |
|
sum += d[types[i]]; |
|
}; |
|
|
|
return sum; |
|
}) |
|
]) |
|
.range([h - padding, padding / 2]) |
|
.nice(); |
|
|
|
//Define X axis |
|
xAxis = d3.axisBottom() |
|
.scale(xScale) |
|
.ticks(10) |
|
.tickFormat(formatTime); |
|
|
|
//Define Y axis |
|
yAxis = d3.axisRight() |
|
.scale(yScale) |
|
.ticks(5); |
|
|
|
//Define area |
|
area = d3.area() |
|
.x(function(d) { return xScale(d.data.date); }) |
|
.y0(function(d) { return yScale(d[0]); }) |
|
.y1(function(d) { return yScale(d[1]); }); |
|
|
|
//Create SVG element |
|
var svg = d3.select("body") |
|
.append("svg") |
|
.attr("width", w) |
|
.attr("height", h); |
|
|
|
//Create areas for each nation |
|
svg.append("g") |
|
.attr("id", "nations"); |
|
|
|
//Create areas for TYPES |
|
svg.append("g") |
|
.attr("id", "types") |
|
.selectAll("path") |
|
.data(typeSeries, key) |
|
.enter() |
|
.append("path") |
|
.attr("class", "area") |
|
.attr("opacity", 1) |
|
.attr("d", area) |
|
.attr("fill", function(d) { |
|
|
|
//Which type |
|
var thisType = d.key; |
|
|
|
//New color var |
|
var color; |
|
|
|
switch (thisType) { |
|
case "OECD": |
|
color = "rgb(110, 64, 170)"; |
|
break; |
|
case "nonOECD": |
|
color = "rgb(76, 110, 219)"; |
|
break; |
|
} |
|
|
|
return color; |
|
}) |
|
.on("click", function(d) { |
|
|
|
//Update view state |
|
viewState++; |
|
|
|
//TYPES |
|
|
|
//Which type was clicked? |
|
var thisType = d.key; |
|
|
|
viewType = thisType; |
|
|
|
//Generate a new data set with all-zero values, except the type data |
|
thisTypeDataset = []; |
|
|
|
for (var i = 0; i < typeDataset.length; i++) { |
|
thisTypeDataset[i] = { |
|
date: typeDataset[i].date, |
|
OECD: 0, |
|
nonOECD: 0, |
|
[thisType]: typeDataset[i][thisType] |
|
} |
|
} |
|
|
|
var thisTypeSeries = typeStack(thisTypeDataset); |
|
|
|
//Bind the new data set to paths, overwriting old bound data. |
|
var paths = d3.selectAll("#types path") |
|
.data(thisTypeSeries, key) |
|
.classed("unclickable", true); |
|
|
|
//Transition areas into new positions |
|
var areaTransitions = paths.transition() |
|
.duration(1000) |
|
.attr("d", area); |
|
|
|
//Update scale |
|
yScale.domain([ |
|
0, |
|
d3.max(thisTypeDataset, function(d) { |
|
var sum = 0; |
|
|
|
sum += d[thisType]; |
|
|
|
return sum; |
|
}) |
|
]); |
|
|
|
//Append this transition |
|
areaTransitions.transition() |
|
.delay(200) |
|
.on("start", function() { |
|
|
|
//Transition axis to new scale concurrently |
|
d3.select("g.axis.y") |
|
.transition() |
|
.duration(1000) |
|
.call(yAxis); |
|
|
|
}) |
|
.duration(1000) |
|
.attr("d", area) |
|
.transition() |
|
.on("start", function() { |
|
//Make nations visible |
|
d3.selectAll("g#nations path") |
|
.attr("opacity", 1); |
|
}) |
|
.duration(1000) |
|
.attr("opacity", 0) |
|
.on("end", function(d, i) { |
|
//Reveal back button |
|
if (i == 0) { |
|
toggleBackButton(); |
|
} |
|
}); |
|
|
|
// Nations |
|
|
|
//Get all possible keys (region + nation) |
|
var keysAll = Object.keys(dataset[0]).slice(1); |
|
|
|
//Loop once for each key |
|
var keysOfThisType = []; |
|
for (var i = 0; i < keysAll.length; i++) { |
|
if (dataset[0][keysAll[i]].type == thisType) { |
|
keysOfThisType.push(keysAll[i]); |
|
} |
|
} |
|
|
|
//Give the new keys to the stack function |
|
nationStack.keys(keysOfThisType) |
|
.value(function value(d, key) { |
|
return d[key].consumption; |
|
}); |
|
|
|
//Stack the data |
|
var nationSeries = nationStack(dataset); |
|
|
|
//Create areas for each nation |
|
svg.select("g#nations") |
|
.selectAll("path") |
|
.data(nationSeries, key) |
|
.enter() |
|
.append("path") |
|
.attr("class", "area") |
|
.attr("opacity", 0) |
|
.attr("d", area) |
|
.attr("fill", function(d, i) { |
|
|
|
//Pick the nation |
|
var thisKey = d.key; |
|
|
|
var thisType = d[0].data[thisKey].type; |
|
|
|
//Find a cool color |
|
var spread = 0.4; |
|
var startingPoint; |
|
|
|
//Choose the color spectrum |
|
switch (thisType) { |
|
case "OECD": |
|
startingPoint = 0; |
|
break; |
|
case "nonOECD": |
|
startingPoint = 0.4; |
|
break; |
|
} |
|
|
|
//How many cars? |
|
var numNations = keysOfThisType.length; |
|
|
|
//Get a value between 0.0 and 1.0 |
|
var normalized = startingPoint + ((i / numNations) * spread); |
|
|
|
//Get that color on the spectrum |
|
return d3.interpolateCool(normalized); |
|
}) |
|
.on("click", function(d) { |
|
|
|
//Update view state |
|
viewState++; |
|
|
|
//Hide the back button during this view transition |
|
toggleBackButton(); |
|
|
|
var thisType = d.key; |
|
|
|
//Fade out all other nation |
|
d3.selectAll("g#nations path") |
|
.classed("unclickable", true) |
|
.filter(function(d) { |
|
if (d.key !== thisType) { |
|
return true; |
|
} |
|
}) |
|
.transition() |
|
.duration(1000) |
|
.attr("opacity", 0); |
|
|
|
//Define area for single nation |
|
var singleNationArea = d3.area() |
|
.x(function(d) { return xScale(d.data.date); }) |
|
.y0(function(d) { return yScale(0); }) |
|
.y1(function(d) { return yScale(d.data[thisType].consumption); }); |
|
|
|
//Use this new area generator to transition the area downward |
|
var thisAreaTransition = d3.select(this) |
|
.transition() |
|
.delay(1000) |
|
.duration(1500) |
|
.attr("d", singleNationArea); |
|
|
|
//Update y scale domain |
|
yScale.domain([ |
|
0, |
|
d3.max(dataset, function(d) { |
|
return d[thisType].consumption; |
|
}) |
|
]); |
|
|
|
//Transitions the clicked area and y axis into place, to fit the new domain |
|
thisAreaTransition |
|
.transition() |
|
.duration(1500) |
|
.attr("d", singleNationArea) |
|
.on("start", function() { |
|
|
|
//Transition axis to new scale |
|
d3.select("g.axis.y") |
|
.transition() |
|
.duration(1500) |
|
.call(yAxis); |
|
|
|
}) |
|
.on("end", function() { |
|
//Restore clickability |
|
d3.select(this).classed("unclickable", "false"); |
|
|
|
//Reveal back button |
|
toggleBackButton(); |
|
}); |
|
|
|
}) |
|
.append("title") //Make tooltip |
|
.text(function(d) { |
|
return d.key; |
|
}); |
|
|
|
}) |
|
.append("title") //Make tooltip |
|
.text(function(d) { |
|
return d.key; |
|
}); |
|
|
|
//Create axes |
|
svg.append("g") |
|
.attr("class", "axis x") |
|
.attr("transform", "translate(0," + (h - padding) + ")") |
|
.call(xAxis); |
|
|
|
svg.append("g") |
|
.attr("class", "axis y") |
|
.attr("transform", "translate(" + (w - padding * 2) + ",0)") |
|
.call(yAxis); |
|
|
|
//Create back button |
|
var backButton = svg.append("g") |
|
.attr("id", "backButton") |
|
.attr("opacity", 0) |
|
.classed("unclickable", true) |
|
.attr("transform", "translate(" + xScale.range()[0] + "," + yScale.range()[1] + ")"); |
|
|
|
backButton.append("rect") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("rx", 5) |
|
.attr("rx", 5) |
|
.attr("width", 70) |
|
.attr("height", 30); |
|
|
|
backButton.append("text") |
|
.attr("x", 7) |
|
.attr("y", 20) |
|
.html("← Back"); |
|
|
|
//Define click behavior |
|
backButton.on("click", function() { |
|
|
|
//Hide the back button, as it was just clicked |
|
toggleBackButton(); |
|
|
|
if (viewState == 1) { |
|
//Go back to default view |
|
|
|
//Update view state |
|
viewState--; |
|
|
|
//Re-bind type data and fade in types |
|
var typeAreaTransitions = d3.selectAll("g#types path") |
|
.data(typeSeries, key) |
|
.transition() |
|
.duration(250) |
|
.attr("opacity", 1) |
|
.on("end", function() { |
|
d3.selectAll("g#nations path").remove(); |
|
}); |
|
|
|
//Set y scale back to original domain |
|
yScale.domain([ |
|
0, |
|
d3.max(typeDataset, function(d) { |
|
var sum = 0; |
|
|
|
//Loops once for each row, to calculate total consumption |
|
for (var i = 0; i < types.length; i++) { |
|
sum += d[types[i]]; |
|
}; |
|
|
|
return sum; |
|
}) |
|
]); |
|
|
|
//Transition type areas and y scale back into place |
|
typeAreaTransitions.transition() |
|
.duration(1000) |
|
.on("start", function() { |
|
|
|
//Transition axis to new scale concurrently |
|
d3.select("g.axis.y") |
|
.transition() |
|
.duration(1000) |
|
.call(yAxis); |
|
|
|
}) |
|
.attr("d", area) |
|
.on("end", function() { |
|
d3.select(this).classed("unclickable", false); |
|
}); |
|
|
|
} else if (viewState == 2) { |
|
|
|
//Update view state |
|
viewState--; |
|
|
|
//Restore the old y scale |
|
yScale.domain([ |
|
0, |
|
d3.max(thisTypeDataset, function(d) { |
|
var sum = 0; |
|
|
|
//Calculate the total (sum) of consumption of this type |
|
sum += d[viewType]; |
|
|
|
return sum; |
|
}) |
|
]); |
|
|
|
//Transition the y axis and visible area back into place |
|
d3.selectAll("g#nations path") |
|
.transition() |
|
.on("start", function() { |
|
|
|
//Transition y axis to new scale concurrently |
|
d3.select("g.axis.y") |
|
.transition() |
|
.duration(1000) |
|
.call(yAxis); |
|
|
|
}) |
|
.duration(1000) |
|
.attr("d", area) |
|
.transition() |
|
.duration(1000) |
|
.attr("opacity", 1) |
|
.on("end", function(d, i) { |
|
|
|
//Restore clickability |
|
d3.select(this).classed("unclickable", false); |
|
|
|
//Reveal back button |
|
if (i == 0) { |
|
toggleBackButton(); |
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
var toggleBackButton = function() { |
|
|
|
//Select the button |
|
var backButton = d3.select("#backButton"); |
|
|
|
//Is the button hidden right now? |
|
var hidden = backButton.classed("unclickable"); |
|
|
|
//Decide whether to reveal or hide it |
|
if (hidden) { |
|
|
|
//Reveal it |
|
|
|
//Set up dynamic button text |
|
var buttonText = "← Back to "; |
|
|
|
//Text varies by mode and type |
|
if (viewState == 1) { |
|
buttonText += "home"; |
|
} else if (viewState == 2) { |
|
buttonText += "all " + viewType + " nations"; |
|
} |
|
|
|
//Set text |
|
backButton.select("text").html(buttonText); |
|
|
|
//Resize button depending on text width |
|
var rectWidth = Math.round(backButton.select("text").node().getBBox().width + 16); |
|
backButton.select("rect").attr("width", rectWidth); |
|
|
|
//Fade button in |
|
backButton.classed("unclickable", false) |
|
.transition() |
|
.duration(500) |
|
.attr("opacity", 1); |
|
|
|
} else { |
|
|
|
//Hide it |
|
backButton.classed("unclickable", true) |
|
.transition() |
|
.duration(200) |
|
.attr("opacity", 0); |
|
|
|
} |
|
|
|
}; |
|
|
|
</script> |
|
</body> |
|
</html> |