xxxxxxxxxx
<html>
<head>
<style>
body {
font-family: Roboto;
}
/* Header styling */
#title {
font-family: "Noto Sans";
font-size: 42;
line-height: 110%;
}
#sub-title {
margin: 5px 0px 10px 0px;
width: 375px;
color: #afafaf;
}
.red {
color: #9C454C;
font-weight: bold;
}
.blue {
color: #4577A3;
font-weight: bold;
}
/* Footer style */
a {
text-align: right;
}
/* Tooltip formating */
.flex {
display: flex;
justify-content: space-between;
}
.flex .left {
padding-top: 5px;
margin-left: 25px;
}
.flex .right {
padding-top: 5px;
margin-right: 25px;
}
#tooltip {
display: none;
height: 125px;
width: 192px;
border-radius: 4px;
border: 1px solid #bbbbbb;
background-color: rgb(255, 255, 255);
box-shadow: 1px 1px 1px 0 rgba(0,0,0,0.4);
font-family: Roboto;
font-size: 12px;
}
.tt-header {
font-size: 20px;
font-family: Noto Sans;
text-align: center;
margin: 10 auto 0 auto;
}
#net {
font-size: 12px;
color: #939698;
text-align: center;
margin: 0 auto 5 auto;
}
/* y-axis formatting */
.y-axis, text {
font-weight: 400;
font-size: 12px;
color: #CECECE;
}
.y-axis path {
display: none;
}
.tick line {
stroke: #F1F1F1;
}
.v-line {
stroke: black;
stroke-dasharray: 3;
opacity: .2;
}
/* Mark formatting */
.pos.barbell{
stroke: #4577A3;
}
.neg.barbell{
stroke: #9C454C;
}
.barbell line{
stroke-width: 3;
}
.barbell line{
stroke-width: 3;
}
.pos circle {
fill: #4577A3;
}
.neg circle {
fill: #9C454C;
}
.pos circle {
fill: #4577A3;
}
.neg circle {
fill: #9C454C;
}
/* Lable formatting */
.outflow-lable {
text-anchor: middle;
font-size: 12px;
font-weight: 400;
fill: #9C454C;
opacity: .5;
}
.inflow-lable {
text-anchor: middle;
font-size: 12px;
font-weight: 400;
fill: #4577A3;
opacity: .5;
}
</style>
<link rel="stylesheet" href="skeleton.css">
<link href="https://fonts.googleapis.com/css?family=Noto+Sans:700|Roboto" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
</head>
<body>
<div class="container">
<div class="header">
<div id="title">Are More People Coming or Going?</div>
<div id="sub-title">Below we look at how many people flowed <span class="blue">into</span> or <span class="red">out</span> of a short list of countries in 2015.</div>
</div>
<select class="u-full-width" id="country">
<option>Australia</option>
<option>Colombia</option>
<option>France</option>
<option>Indonesia</option>
<option>Spain</option>
<option>United Kingdom</option>
</select>
<div class="chart"></div>
<div id="tooltip">
<div class="tt-header"></div>
<div id="net"></div>
<div id="outflow" class="flex">
<div class="left" id="outflow"></div>
<div class="right" id="outflow"></div>
</div>
<div id="inflow" class="flex">
<div class="left"></div>
<div class="right"></div>
</div>
</div>
<button class="u-full-width scale">Change to Log Scale</button>
<a class="twelve columns" href="https://stats.oecd.org/Index.aspx?DataSetCode=MIG">OECD.Stat</a>
</div>
</body>
<script>
class slopeChart {
constructor(opts) {
// Make the opts available locally
this.element = opts.element;
this.svgWidth = opts.width;
this.svgHeight = opts.height;
this.margin = opts.margin;
this.padding = opts.padding;
this.data = opts.transformedData;
// Create additional setup variables we'll need locally based on the opts
this.plotWidth = this.svgWidth - (this.margin.left + this.margin.right);
this.plotHeight = this.svgHeight - (this.margin.top + this.margin.bottom);
this.chartWidth = this.plotWidth - (this.padding.right + this.padding.left);
this.chartHeight = this.plotHeight - (this.padding.top + this.padding.bottom);
this.draw();
}
draw() {
const svg = d3.select(this.element)
.append('svg')
.attr("width", this.svgWidth)
.attr("height", this.svgHeight)
.attr("translate", `transform(${[this.margin.left, this.margin.top]})`);
this.plot = svg.selectAll("g").data([null]);
this.plot2 = svg.append('g')
.classed("decoration-layer", true)
.attr("translate", `transform(${[this.padding.left, this.padding.top]}))`);
this.plotEnter = this.plot.enter().append('g')
.classed("axis-layer", true)
.attr("translate", `transform(${[this.padding.left, this.padding.top]}))`);
this.plotEnter2 = this.plot.enter().append('g')
.classed("mark-layer", true)
.attr("translate", `transform(${[this.padding.left, this.padding.top]}))`);
this.addDecorations();
this.updateScales();
this.gupAxes();
this.gupBarbells();
}
addDecorations() {
////////////////////////////////////////////////////////////////////////////////
///////////////////////////// VERTICAL DASHED LINES ////////////////////////////
////////////////////////////////////////////////////////////////////////////////
this.plot2
.append("line")
.classed("v-line", true)
.attr("x1", this.plotWidth - (this.padding.right + this.margin.right + 12))
.attr("y1", 0)
.attr("x2", this.plotWidth - (this.padding.right + this.margin.right + 12))
.attr("y2", this.chartHeight)
////////////////////////////////////////////////////////////////////////////////
//////////////////////// LABELS BELOW THE VERTICAL LINES ///////////////////////
////////////////////////////////////////////////////////////////////////////////
this.plot2
.append("line")
.classed("v-line", true)
.attr("x1", this.chartWidth * .25 + this.padding.left + this.margin.left)
.attr("y1", 0)
.attr("x2", this.chartWidth * .25 + this.padding.left + this.margin.left)
.attr("y2", this.chartHeight);
const outflowLabel = this.plot2
.append("text")
.classed("outflow-lable", true)
.attr("x", this.margin.left + this.padding.left + (this.chartWidth * .25))
.attr("y", this.chartHeight)
.attr("dy", 30)
.text("OUTFLOW")
.on("mouseover.highlight", function(d) {
d3.select(this)
.style("opacity", 1);
d3.selectAll(".neg")
.style("opacity", 1);
d3.selectAll(".pos")
.style("opacity", .3);
})
.on("mouseout.highlight", function(d) {
d3.selectAll(".barbell")
.style("opacity", .5);
});
const inflowLabel = this.plot2
.append("text")
.classed("inflow-lable", true)
.attr("x", this.margin.left + this.padding.left + (this.chartWidth * .75))
.attr("y", this.chartHeight)
.attr("dy", 30)
.text("INFLOW")
.on("mouseover.highlight", function(d) {
d3.select(this)
.style("opacity", 1);
d3.selectAll(".pos")
.style("opacity", 1);
d3.selectAll(".neg")
.style("opacity", .3);
})
.on("mouseout.highlight", function(d) {
d3.selectAll(".pos")
.style("opacity", .5);
d3.selectAll(".neg")
.style("opacity", .5);
});
}
updateScales() {
////////////////////////////////////////////////////////////////////////////////
/////////////////// FIND THE MIN AND MAX OF ALL MOVEMENT ///////////////////////
////////////////////////////////////////////////////////////////////////////////
const migration = [];
for(const record of this.data) {
migration.push(record.outflow);
migration.push(record.inflow);
}
[this.minM, this.maxM] = d3.extent(migration);
////////////////////////////////////////////////////////////////////////////////
////////////////////// SET SCALES BASED ON BUTTON TEXT /////////////////////////
////////////////////////////////////////////////////////////////////////////////
if (document.querySelector("button.scale").textContent.includes("Linear")) {
this.yScale = d3.scaleLog()
.domain([this.minM, this.maxM])
.range([this.chartHeight, this.margin.top + this.padding.top]);
} else {
this.yScale = d3.scaleLinear()
.domain([this.minM, this.maxM])
.range([this.chartHeight, this.margin.top + this.padding.top]);
}
}
gupAxes() {
this.yAxis = d3.axisRight()
.scale(this.yScale)
.ticks(10, d3.format(",k"))
.tickSize( (this.margin.left + this.padding.left) - this.chartWidth * .85);
this.yAxisG = this.plotEnter.selectAll('g').data([null]);
this.yAxisGEnter = this.yAxisG.enter().append('g')
.attr("class", "y-axis")
.attr("transform", `translate(${this.chartWidth}, 0 )`)
const t = d3.transition().duration(750);
this.yAxisGEnter.exit().transition(t).remove();
this.yAxisGEnter
.merge(this.yAxisG)
.transition(t)
.call(this.yAxis);
}
gupBarbells() {
// This is our UPDATE selection since it's the data join
this.groups = this.plotEnter2.selectAll("g")
.data(this.data, d => d.country)
this.groups.exit().remove();
this.gEnter = this.groups.enter().append("g")
.attr("class", d => `${d["pos-or-neg"]} barbell`)
.style("opacity", .5);
this.gEnter
.merge(this.gEnter);
////////////////////////////////////////////////////////////////////////////////
//////////////////////////// HIGHLIGHT ACTIVE BARBELLS /////////////////////////
////////////////////////////////////////////////////////////////////////////////
this.gEnter
// Dim other marks to bring the hover mark into focus
.on("mouseover.highlight", function(d) {
this.classList.add("hovered")
d3.selectAll(".barbell")
.style("opacity", .3);
d3.select(".hovered")
.style("opacity", 1);
})
// Reset to the default
.on("mouseout.highlight", function(d) {
this.classList.remove("hovered")
d3.selectAll(".barbell")
.style("opacity", .5);
})
////////////////////////////////////////////////////////////////////////////////
//////////////////////////////// REVEAL TOOLTIPS ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////
const tooltip = d3.select("#tooltip");
this.gEnter
.on("mousemove.tooltip", function(d) {
tooltip.transition()
.style("display", "block")
.style("opacity", .9);
const commaFormat = d3.format(",");
// Positioning the tooltip to the bottom right of your cursor
tooltip
.style("position", "fixed")
.style("left", d3.event.clientX + 20)
.style("top", d3.event.clientY + 20);
// Creating the country header
tooltip.select(".tt-header")
.text(d.country);
// Add the the subtitle of Net value
tooltip.select("#net")
.text(`Net ${commaFormat(d.net)}`)
// Insert row of data for Outflow
tooltip.select("#outflow").select(".left")
.text("OUTFLOW")
tooltip.select("#outflow").select(".right")
.text(commaFormat(d.outflow))
// Insert row of data for Inflow
tooltip.select("#inflow").select(".left")
.text("INFLOW")
tooltip.select("#inflow").select(".right")
.text(commaFormat(d.inflow))
})
.on("mouseout.tooltip", function() {
tooltip.style("display", "none");
});
const t = d3.transition().duration(750);
this.lines = this.gEnter.append("line")
.attr("x1", this.margin.left + this.padding.left + (this.chartWidth * .25))
.attr("x2", this.margin.left + this.padding.left + (this.chartWidth * .75))
.attr("y1", d => this.yScale(d.outflow))
.attr("y2", d => this.yScale(d.inflow))
.merge(this.groups.select("line"))
.transition(t)
.attr("y1", d => this.yScale(d.outflow))
.attr("y2", d => this.yScale(d.inflow));
this.circlesOut = this.gEnter.append("circle")
.classed("circle-out", true)
.attr("r", 4)
.attr("cx", this.margin.left + this.padding.left + (this.chartWidth * .25))
.attr("cy", d => this.yScale(d.outflow))
.merge(this.groups.select('.circle-out'))
.transition(t)
.attr("cy", d => this.yScale(d.outflow));
this.circlesIn = this.gEnter.append("circle")
.classed("circle-in", true)
.attr("r", 4)
.attr("cx", this.margin.left + this.padding.left + (this.chartWidth * .75))
.attr("cy", d => this.yScale(d.inflow))
.merge(this.groups.select('.circle-in'))
.transition(t)
.attr("cy", d => this.yScale(d.inflow));
}
updateButtonText() {
if (document.querySelector("button.scale").textContent.includes("Log")) {
document.querySelector("button.scale").textContent = "Change to Linear Scale";
} else {
document.querySelector("button.scale").textContent = "Change to Log Scale";
}
}
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////// LOAD DATA FOR THE 1ST TIME //////////////////////////
////////////////////////////////////////////////////////////////////////////////
d3.csv('OECD-data.csv').then( data => {
function initializeData(data) { // , key = document.querySelector("#country").value) {
// Group data by each country in our list
const nested = d3.nest()
.key(d => d["country"])
.entries(data);
// Filter data by the current country selection
const filtered = nested
.filter( d => d.key === document.querySelector("#country").value);
// Initialize a new data array to use in our visualization
const transformedData = filtered[0].values.map( d => {
return {
country: d["foreign nationality"],
outflow: +d.outflow,
inflow: +d.inflow,
net: +d.inflow - +d.outflow,
"pos-or-neg": +d.inflow - +d.outflow >= 0 ? "pos" : "neg"
}
})
return transformedData;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////// SET THE CHART SIZE //////////////////////////////
////////////////////////////////////////////////////////////////////////////////
const width = 500;
const height = 450;
const margin = {top: 10, right: 50, bottom: 0, left: 30};
const padding = {top: 10, right: 20, bottom: 20, left: 30};
////////////////////////////////////////////////////////////////////////////////
/////////////////////////////// CREATE NEW CHART ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////
const chart = new slopeChart({
element: document.querySelector('.chart'),
width,
height,
margin,
padding,
transformedData: initializeData(data)
});
////////////////////////////////////////////////////////////////////////////////
///////////////////////////// HAND CONTROL CHANGES /////////////////////////////
////////////////////////////////////////////////////////////////////////////////
d3.select('#country').on('change', () => {
chart.data = initializeData(data);
chart.updateScales();
chart.gupAxes();
chart.gupBarbells();
});
d3.selectAll('button.scale').on('click', () => {
chart.updateButtonText();
chart.updateScales();
chart.gupAxes();
chart.gupBarbells();
});
})
</script>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js