Skip to content

Instantly share code, notes, and snippets.

@officeofjane
Created July 11, 2019 12:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save officeofjane/8092eaec170663cadea5da1647ca77aa to your computer and use it in GitHub Desktop.
Save officeofjane/8092eaec170663cadea5da1647ca77aa to your computer and use it in GitHub Desktop.
Responsive animated line chart
license: mit
function debounce(func, wait, immediate){
var timeout, args, context, timestamp, result;
if (null == wait) wait = 100;
function later() {
var last = Date.now() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
context = args = null;
}
}
};
var debounced = function(){
context = this;
args = arguments;
timestamp = Date.now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
debounced.clear = function() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
debounced.flush = function() {
if (timeout) {
result = func.apply(context, args);
context = args = null;
clearTimeout(timeout);
timeout = null;
}
};
return debounced;
};
year Highlight CategoryA CategoryB
1990 50 80 50
1991 53 80 50
1992 58 80 60
1993 60 95 80
1994 70 100 80
1995 81 105 50
1996 85 120 80
1997 86 125 120
1998 86 130 60
1999 88 130 70
2000 91 125 10
2001 92 125 20
2002 95 120 20
2003 100 150 40
2004 180 200 60
2005 200 250 100
2006 260 350 110
2007 300 500 200
2008 360 600 -80
2009 350 400 160
2010 370 600 370
2011 400 750 180
2012 410 650 375
2013 425 700 265
2014 450 620 280
2015 445 600 50
2016 410 550 120
2017 430 520 310
2018 450 510 270
2019 525 500 300
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="debounce.js"></script>
<style>
body {
font-family: "avenir next", Arial, sans-serif;
font-size: 12px;
margin: 0;
color: #666;
}
#vis {
min-width: 300px;
max-width: 900px;
margin: 0 auto;
}
.axis path, .axis line {
fill: none;
stroke: #666;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke-width: 1.5px;
}
#line-Highlight {
stroke-width: 2px;
}
.baseline {
stroke: #666;
}
</style>
</head>
<body>
<div id='vis'></div>
<script>
const margin = { top: 50, right: 70, bottom: 30, left: 50 };
const $chart = d3.select('#vis');
const $svg = $chart.append('svg');
const $plot = $svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const parseDate = d3.timeParse('%Y');
// set up scales
const x = d3.scaleTime();
const y = d3.scaleLinear();
const colour = d3.scaleOrdinal(d3.schemeCategory10);
const xAxis = d3.axisBottom()
.scale(x)
.ticks(5);
const yAxis = d3.axisLeft()
.scale(y)
.ticks(10);
const line = d3.line()
.x(d => x(d.date))
.y(d => y(d.value))
.curve(d3.curveMonotoneX);
function render() {
const width = parseInt($chart.node().offsetWidth) - margin.left - margin.right;
const height = parseInt(width * 0.6) - margin.top - margin.bottom;
$svg.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
x.range([0, width]);
y.range([height, 0]);
$plot.select('.axis.x')
.attr('transform', `translate(0, ${height})`)
.call(xAxis)
.select('.domain').remove();
$plot.select('.axis.y')
.call(yAxis)
.call(g => g.select('.tick:last-of-type text').clone()
.attr('x', 3)
.attr('text-anchor', 'start')
.attr('font-weight', 600)
.text('$ billion'))
.select('.domain').remove();
$plot.select('.baseline')
.attr('x1', 0)
.attr('x2', width)
.attr('y1', y(0))
.attr('y2', y(0))
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-width', '1px')
.attr('shape-rendering', 'crispEdges')
.attr('stroke-dasharray', '3, 3')
const path = $plot.selectAll('path')
.attr('d', d => line(d.values))
.attr('stroke', d => colour(d.name))
.attr('opacity', d => d.name == 'Highlight' ? 1 : 0.5)
.attr('id', (d, i) => `line-${d.name}`)
path.each((d, i) => {
const sel = d3.select(`#line-${d.name}`);
const length = sel.node().getTotalLength();
sel.attr('stroke-dasharray', `${length} ${length}`)
.attr('stroke-dashoffset', length)
.transition()
.duration(5000)
.attr('stroke-dashoffset', 0)
})
$plot.selectAll('.line-label')
.attr('transform', d => {
return `translate(${x(d.value.date)}, ${y(d.value.value)})`;
})
.attr('x', 5)
.attr('dy', '.35em')
.attr('fill', d => colour(d.name))
.attr('font-weight', d => d.name == 'Highlight' ? 700 : 400)
.text(d => d.name)
.attr('opacity', 0)
.transition()
.delay(4000)
.duration(200)
.attr('opacity', 1)
}
function bindData(rawdata) {
// column headings, for each line
const keys = rawdata.columns.filter(key => key != 'year');
rawdata.forEach(d => {
d.year = parseDate(d.year);
})
const data = keys.map(name => {
return {
name,
values: rawdata.map(d => {
return {date: d.year, value: +d[name]};
})
}
});
colour.domain(keys);
x.domain(d3.extent(rawdata, d => d.year));
y.domain([
d3.min(data, c => d3.min(c.values, v => v.value)),
d3.max(data, c => d3.max(c.values, v => v.value))
]).nice();
// bind data to DOM elements
const $lines = $plot.append('g')
.attr('class', 'lines')
.selectAll('.line')
.data(data)
.enter()
.append('g')
.attr('class', 'line')
$lines.append('path')
.attr('class', 'path')
$lines.append('text')
.datum(d => {
return {
name: d.name,
value: d.values[d.values.length - 1]
}
})
.attr('class', 'line-label')
.attr('opacity', 0)
$plot.append('g')
.attr('class', 'axis x');
$plot.append('g')
.attr('class', 'axis y');
$plot.append('line')
.attr('class', 'baseline')
window.addEventListener('resize', debounce(render, 200));
render();
}
function init() {
d3.csv('dummy-data.csv').then(bindData);
}
init();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment