D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
robcrock
Full window
Github gist
#KnowledgeGaps | Learning How to Create a Slope Chart
<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