I liked the labelling on this chart from The Financial Times, so recreated it in D3 using SVG's textPath, and using a Makeover Monday dataset.
Built with blockbuilder.org
license: mit |
I liked the labelling on this chart from The Financial Times, so recreated it in D3 using SVG's textPath, and using a Makeover Monday dataset.
Built with blockbuilder.org
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<link href="https://fonts.googleapis.com/css?family=Oxygen" rel="stylesheet"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<style> | |
body { | |
font-family: 'Oxygen', sans-serif; | |
font-size: 12px | |
margin: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
background: #222222 | |
} | |
.line-app { | |
fill: none; | |
stroke-width: 4px; | |
} | |
</style> | |
</head> | |
<body> | |
<script> | |
let appData = [ | |
{ | |
"date": "1/03/2015", | |
"app": "Snapchat", | |
"percentage": 0.11 | |
}, | |
{ | |
"date": "1/09/2015", | |
"app": "Snapchat", | |
"percentage": 0.17 | |
}, | |
{ | |
"date": "1/03/2016", | |
"app": "Snapchat", | |
"percentage": 0.24 | |
}, | |
{ | |
"date": "1/09/2016", | |
"app": "Snapchat", | |
"percentage": 0.35 | |
}, | |
{ | |
"date": "1/03/2017", | |
"app": "Snapchat", | |
"percentage": 0.39 | |
}, | |
{ | |
"date": "1/09/2017", | |
"app": "Snapchat", | |
"percentage": 0.47 | |
}, | |
{ | |
"date": "1/03/2015", | |
"app": "Instagram", | |
"percentage": 0.29 | |
}, | |
{ | |
"date": "1/09/2015", | |
"app": "Instagram", | |
"percentage": 0.29 | |
}, | |
{ | |
"date": "1/03/2016", | |
"app": "Instagram", | |
"percentage": 0.23 | |
}, | |
{ | |
"date": "1/09/2016", | |
"app": "Instagram", | |
"percentage": 0.24 | |
}, | |
{ | |
"date": "1/03/2017", | |
"app": "Instagram", | |
"percentage": 0.23 | |
}, | |
{ | |
"date": "1/09/2017", | |
"app": "Instagram", | |
"percentage": 0.24 | |
}, | |
{ | |
"date": "1/03/2015", | |
"app": "Facebook", | |
"percentage": 0.12 | |
}, | |
{ | |
"date": "1/09/2015", | |
"app": "Facebook", | |
"percentage": 0.13 | |
}, | |
{ | |
"date": "1/03/2016", | |
"app": "Facebook", | |
"percentage": 0.15 | |
}, | |
{ | |
"date": "1/09/2016", | |
"app": "Facebook", | |
"percentage": 0.13 | |
}, | |
{ | |
"date": "1/03/2017", | |
"app": "Facebook", | |
"percentage": 0.11 | |
}, | |
{ | |
"date": "1/09/2017", | |
"app": "Facebook", | |
"percentage": 0.09 | |
}, | |
{ | |
"date": "1/03/2015", | |
"app": "Twitter", | |
"percentage": 0.21 | |
}, | |
{ | |
"date": "1/09/2015", | |
"app": "Twitter", | |
"percentage": 0.18 | |
}, | |
{ | |
"date": "1/03/2016", | |
"app": "Twitter", | |
"percentage": 0.16 | |
}, | |
{ | |
"date": "1/09/2016", | |
"app": "Twitter", | |
"percentage": 0.13 | |
}, | |
{ | |
"date": "1/03/2017", | |
"app": "Twitter", | |
"percentage": 0.11 | |
}, | |
{ | |
"date": "1/09/2017", | |
"app": "Twitter", | |
"percentage": 0.07 | |
} | |
] | |
const formatDate = d3.timeFormat("%b %y") // Mar 17 | |
const parseDate = d3.timeParse("%d/%m/%Y") // 1/03/2017 | |
function formatPercentage(n) { return Math.round(n * 100) + "%"; } | |
appData.forEach(function (d) { | |
d.parsedDate = parseDate(d.date) | |
}) | |
let nestedData = d3.nest() | |
.key(function (d) { return d.app; }) | |
.entries(appData) | |
////////////////////////////////////////////////////////////////// | |
const width = 700 | |
const height = 500 | |
const margin = { "top": 50, "bottom": 50, "left": 50, "right": 150, } | |
let xScale = d3.scaleTime() | |
.domain(d3.extent(appData, function (d) { return d.parsedDate; })) | |
.range([0, width]) | |
let yScale = d3.scaleLinear() | |
.domain([0, 0.5]) | |
.range([height, 0]) | |
let xAxis = d3.axisBottom(xScale) | |
let yAxis = d3.axisLeft(yScale) | |
.ticks(5) | |
let colour = d3.scaleOrdinal() | |
.domain(["Twitter", "Snapchat", "Facebook", "Instagram"]) | |
.range(["#00aced", "#fffc00", "#3b5998", "#cd486b"]) | |
let backgroundColour = "#222222" | |
let line = d3.line() | |
.x(function (d) { return xScale(d.parsedDate); }) | |
.y(function (d) { return yScale(d.percentage); }) | |
.curve(d3.curveCardinal.tension(0.5)); | |
let svg = d3.select("body").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
let g = svg.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
////////////////////////////////////////////////////////////////// | |
let yAxixBoxDate = new Date("1/1/2017") | |
let yAxixBoxWidth = 30 | |
let yAxixBoxHeight = 20 | |
let axes = g.append("g").attr("class", "axes") | |
let yAxisG = axes.append("g") | |
.attr("transform", "translate(0,0)") | |
.call(yAxis) | |
let xAxisG = axes.append("g") | |
.attr("transform", "translate(0," + height + ")") | |
let xTicks = xAxisG.selectAll(".tick") | |
.data(nestedData[0].values ) | |
.enter() | |
.append("g") | |
.attr("class", "tick") | |
.attr("transform", function(d){ return "translate(" + xScale(d.parsedDate) + ",0)"; }) | |
xTicks.append("text") | |
.text(function (d) { return formatDate(d.parsedDate); }) | |
.attr("y", 18) | |
.style("text-anchor", "middle") | |
xTicks.append("line") | |
.attr("y1", -height) | |
.attr("y2", 1) | |
yAxisG.selectAll(".tick").selectAll("text") | |
.text(function (d) { return formatPercentage(d); }) | |
.attr("x", xScale(new Date("1/1/2017"))) | |
.style("text-anchor", "middle") | |
yAxisG.selectAll(".tick").selectAll("line") | |
.attr("x1", 0) | |
.attr("x2", width) | |
yAxisG.selectAll(".tick").append("rect") | |
.attr("x", xScale(yAxixBoxDate) - (yAxixBoxWidth/2)) | |
.attr("y", -(yAxixBoxHeight/2)) | |
.attr("width", yAxixBoxWidth) | |
.attr("height", yAxixBoxHeight) | |
.style("fill", backgroundColour) | |
yAxisG.selectAll(".tick").selectAll("text") | |
.text(function (d) { return formatPercentage(d); }) | |
.attr("x", xScale(new Date("1/1/2017"))) | |
.style("text-anchor", "middle") | |
.raise() | |
axes.selectAll(".domain").remove() | |
axes.selectAll(".tick").selectAll("line") | |
.style("stroke", "grey") | |
.style("stroke-dasharray", "2,2") | |
.style("stroke-linecap", "round") | |
axes.selectAll(".tick").selectAll("text") | |
.style("fill", "grey") | |
////////////////////////////////////////////////////////////////// | |
let lines = g.append("g").attr("class", "lines") | |
let app = lines.selectAll("g") | |
.data(nestedData) | |
.enter() | |
.append("g") | |
.attr("id", function (d) { return d.key }) | |
lines.select("#Snapchat").raise() | |
app.append("path") | |
.datum(function (d) { return d.values; }) | |
.attr("d", line) | |
.attr("class", "line-app") | |
.attr("id", function(d){ return "line-app-" + d[0].app }) | |
.style("stroke", function (d) { return colour(d[0].app); }) | |
.style("stroke-width", "4px") | |
.style("fill", "none") | |
app.selectAll("circle") | |
.data(function (d) { return d.values; }) | |
.enter() | |
.append("circle") | |
.attr("cx", function(d){ return xScale(d.parsedDate) }) | |
.attr("cy", function(d){ return yScale(d.percentage) }) | |
.attr("r", 4) | |
.style("fill", function (d) { return colour(d.app); }) | |
.style("stroke", backgroundColour) | |
.style("stroke-width", 2) | |
app.append("text") | |
.attr("dy", function(d){ return d.key == "Twitter" ? 17 : -7; }) | |
.append("textPath") | |
.text(function (d) { return d.key }) | |
.attr("xlink:href", function(d){ return "#" + "line-app-" + d.key; }) | |
.attr("startOffset", "99%") | |
.attr("text-anchor", "end") | |
.style("fill", function (d) { return colour(d.key); }) | |
////////////////////////////////////////////////////////////////// | |
</script> | |
</body> |