Connected scatterplot with D3
A connected scatterplot is a type of visualization that displays the evolution of a series of data points that are connected by straight line segments. In some cases, it is not the most intuitive to read; but it is impressive for storytelling.
More about: Connected scatterplot
Connected scatterplot
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Include d3 library -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Create a container to host the chart -->
<div id="viz_container"></div>
// set the dimensions and margins of the graph
const margin = {top: 60, right: 60, bottom: 50, left: 50};
const width = 450 - margin.left - margin.right;
const height = 350 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3.select("#viz_container")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 450 350")
.attr("preserveAspectRatio", "xMinYMin")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// parse the Data
d3.csv("https://raw.githubusercontent.com/GDS-ODSSS/unhcr-dataviz-platform/master/data/correlation/scatterplot_connected.csv")
.then(function(data){
// list of value keys
const typeKeys = data.columns.slice(1);
// X scale and Axis
const f = d3.format("~s")
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => +d.refugee_number)).nice()
.range([0, width])
svg
.append('g')
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(xScale).tickSize(0).ticks(5).tickPadding(8).tickFormat(f))
.call(d => d.select(".domain").remove());
// Y scale and Axis
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => +d.idp_number)).nice()
.range([height, 0]);
svg
.append('g')
.call(d3.axisLeft(yScale).tickSize(0).ticks(3).tickPadding(4).tickFormat(f))
.call(d => d.select(".domain").remove());
// bubble size scale
const zScale = d3.scaleSqrt()
.domain([600000, 160000000])
.range([2, 50])
// color palette
const color = d3.scaleOrdinal()
.domain(typeKeys)
.range(["#EF4A60", "#8EBEFF", "#00B398", "#E1CC0D", "#589BE5", "#18375F", "#0072BC"])
// set horizontal grid line
const GridLineH = () => d3.axisLeft().scale(yScale);
svg
.append("g")
.attr("class", "grid")
.call(GridLineH()
.tickSize(-width,0,0)
.tickFormat("")
.ticks(3)
);
// set vertical grid line
const GridLineV = () => d3.axisBottom().scale(xScale);
svg
.append("g")
.attr("class", "grid")
.call(GridLineV()
.tickSize(height,0,0)
.tickFormat("")
.ticks(5)
);
// create a tooltip
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip");
// tooltip events
const mouseover = function(d) {
tooltip
.style("opacity", 1)
d3.select(this)
.style("stroke", "#EF4A60")
.style("opacity", 0.5)
};
const mousemove = function(event,d) {
const f = d3.format(",");
const t = d3.format("Y");
tooltip
.html("<div style='color: #0072BC'<b>" +t(d.year)+ "</b></div><div><b>Refugees:</b> " +f(d.refugee_number)+ "</div><div><b>IDPs:</b> "+ f(d.idp_number) +"</div>")
.style("top", event.pageY - 10 + "px")
.style("left", event.pageX + 10 + "px")
};
const mouseleave = function(d) {
tooltip
.style("opacity", 0)
d3.select(this)
.style("stroke", "none")
.style("opacity", 1)
};
// create graph
svg.append("g")
.selectAll("g")
.data(data)
.join("circle")
.attr("cx", d => xScale(+d.refugee_number))
.attr("cy", d => yScale(+d.idp_number))
.attr("r", 3)
.style("fill", "#0072BC")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
const arrowPoints = [[0, 0], [0, 20], [20, 10]];
svg
.append('defs')
.append('marker')
.attr('id', 'arrow')
.attr('viewBox', [0, 0, 20, 20])
.attr('refX', 10)
.attr('refY', 10)
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', d3.line()(arrowPoints))
.attr('fill', '#0072BC');
svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#0072BC")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(d => xScale(+d.refugee_number))
.y(d => yScale(+d.idp_number))
)
.attr('marker-end', 'url(#arrow)')
.attr('fill', 'none')
// set title
svg
.append("text")
.attr("class", "chart-title")
.attr("x", -(margin.left)*0.4)
.attr("y", -(margin.top)/1.5)
.attr("text-anchor", "start")
.text("Evolution of refugee vs IDP population in Afghanistan | 2001-2021")
// set X axis label
svg
.append("text")
.attr("class", "chart-label")
.attr("x", width/1.7)
.attr("y", height+margin.bottom*0.6)
.attr("text-anchor", "middle")
.text("Number of refugees (millions)");
// set Y axis label
svg
.append("text")
.attr("class", "chart-label")
.attr("x", -(margin.left)*0.4)
.attr("y", -(margin.top/8))
.attr("text-anchor", "start")
.text("Number of IDPs (millions)")
// set source
svg
.append("text")
.attr("class", "chart-source")
.attr("x", -(margin.left)*0.4)
.attr("y", height + margin.bottom*0.8)
.attr("text-anchor", "start")
.text("Source: UNHCR")
// set copyright
svg
.append("text")
.attr("class", "copyright")
.attr("x", -(margin.left)*0.4)
.attr("y", height + margin.bottom*0.95)
.attr("text-anchor", "start")
.text("©UNHCR, The UN Refugee Agency")
})