Skip to content

Instantly share code, notes, and snippets.

@anbnyc
Created April 18, 2023 23:55
Show Gist options
  • Save anbnyc/f02244c0c0995bd949b7ef6f7eeb2b7a to your computer and use it in GitHub Desktop.
Save anbnyc/f02244c0c0995bd949b7ef6f7eeb2b7a to your computer and use it in GitHub Desktop.
Animation with d3 tutorial

This is a brief illustration of how to use animate with d3.js using two examples:

  1. A line chart that animates on load.
  2. A bar chart that is a cross-filter for the scatterplot below it, which animates in and out.

This was created for a workshop for the M.S. Data Visualization program at Parsons School of Design. Note the data was auto-generated by ChatGPT and is not intended to be real data.

<!DOCTYPE html>
<html>
<head>
<title>MSDV - D3 demo</title>
<link rel="stylesheet" href="style.css">
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<h1>D3 demo</h1>
</body>
<script src="./index.js"></script>
</html>
/* global d3 */
let state = {
data: [],
filter: [],
};
function setState(newState) {
console.log('setState:', newState)
state = {
...state,
...newState,
}
draw();
}
const HEIGHT = 400;
const WIDTH = 600;
const MARGIN = 20;
function setup() {
d3.select('body')
.selectAll('svg')
.data(['line-chart', 'bar-chart', 'scatterplot'])
.join('svg')
.attr('class', d => d)
.attr('height', `${HEIGHT}px`)
.attr('width', `${WIDTH}px`);
Promise.all([
d3.json('./stocks.json'),
d3.json('./scatter.json')
]).then(([stocksData, scatterData], err) => {
if (err){
console.error(err);
}
const scaledData = stocksData.map(d => {
const startValue = +d.data[0].value
return {
...d,
data: d.data.map(({ timestamp, value }) => ({
timestamp: new Date(timestamp),
value: +value,
index: (value - startValue) / startValue
}))
}
})
setState({ data: scaledData, scatter: scatterData })
})
}
function draw() {
const timeRange = d3.extent(state.data.flatMap(d => d.data).map(d => d.timestamp))
const valueRange = d3.extent(state.data.flatMap(d => d.data).map(d => d.index))
const tickers = [...new Set(state.data.map(d => d.ticker))];
const xTimeScale = d3.scaleLinear()
.domain(timeRange)
.range([MARGIN, WIDTH - MARGIN]);
const yScale = d3.scaleLinear()
.domain(valueRange)
.range([HEIGHT - MARGIN, MARGIN]);
const colorScale = d3.scaleOrdinal()
.domain(tickers)
.range(d3.schemeCategory10);
const xBarScale = d3.scaleBand()
.paddingInner(0.1)
.domain(tickers)
.range([MARGIN, WIDTH - MARGIN]);
const line = d3.line()
.x(function(d) {
return xTimeScale(d.timestamp);
})
.y(function(d) {
return yScale(d.index);
});
const lineGroup = d3.select('svg.line-chart')
.selectAll('g.lines')
.data([null])
.join('g')
.attr('class', 'lines')
const path = lineGroup.selectAll('path')
.data(state.data)
.join('path')
.attr('stroke', d => colorScale(d.ticker))
.attr('d', d => line(d.data))
path
.attr('stroke-dasharray', function(d) {
return `${this.getTotalLength()} ${this.getTotalLength()}`
})
.attr('stroke-dashoffset', function(d){
return this.getTotalLength()
})
.transition()
.duration(1500)
.attr('stroke-dashoffset', 0);
const bar = d3.select('svg.bar-chart')
.selectAll('rect')
.data(state.data)
.join('rect')
.attr('x', d => `${xBarScale(d.ticker)}px`)
.attr('y', d => `${yScale(d.data[d.data.length - 1].index)}px`)
.attr('width', xBarScale.bandwidth())
.attr('height', d => `${HEIGHT - yScale(d.data[d.data.length - 1].index)}px`)
.attr('fill', d => colorScale(d.ticker))
.classed('rect-with-border', d => state.filter.includes(d.ticker))
.on('click', (event, d) => {
const filterIndex = state.filter.indexOf(d.ticker);
if (filterIndex !== -1) {
setState({
filter: [
...state.filter.slice(0, filterIndex),
...state.filter.slice(filterIndex + 1)
]
})
} else {
setState({
filter: [...state.filter, d.ticker]
})
}
});
const xRange = d3.extent(state.scatter.map(d => d.x))
const yRange = d3.extent(state.scatter.map(d => d.y))
const yScatterScale = d3.scaleLinear()
.domain(yRange)
.range([HEIGHT - MARGIN, MARGIN]);
const xScatterScale = d3.scaleLinear()
.domain(xRange)
.range([MARGIN, WIDTH - MARGIN]);
const scatterGroup = d3.select('svg.scatterplot')
.selectAll('circle')
.data(state.scatter)
.join('circle')
.attr('r', 5)
.attr('cx', d => `${xScatterScale(d.x)}px`)
.attr('fill', d => colorScale(d.name))
.transition()
.duration(750)
.attr('opacity', d => state.filter.includes(d.name) ? 1 : 0.25)
.attr('cy', d => state.filter.includes(d.name)
? `${yScatterScale(d.y)}px`
: `${HEIGHT - MARGIN}px`)
}
setup();
[
{ "name": "AAPL", "x": 1, "y": 1000 },
{ "name": "AAPL", "x": 2, "y": 1500 },
{ "name": "AAPL", "x": 3, "y": 1800 },
{ "name": "AAPL", "x": 4, "y": 2000 },
{ "name": "GOOG", "x": 1, "y": 1200 },
{ "name": "GOOG", "x": 2, "y": 1700 },
{ "name": "GOOG", "x": 3, "y": 2000 },
{ "name": "GOOG", "x": 4, "y": 2200 },
{ "name": "TSLA", "x": 1, "y": 800 },
{ "name": "TSLA", "x": 2, "y": 1200 },
{ "name": "TSLA", "x": 3, "y": 1400 },
{ "name": "TSLA", "x": 4, "y": 1700 },
{ "name": "AMZN", "x": 1, "y": 1500 },
{ "name": "AMZN", "x": 2, "y": 2000 },
{ "name": "AMZN", "x": 3, "y": 2300 },
{ "name": "AMZN", "x": 4, "y": 2500 }
]
[
{
"ticker": "AAPL",
"data": [
{ "timestamp": "2023-04-18 09:00:00", "value": 125.21 },
{ "timestamp": "2023-04-18 10:00:00", "value": 126.43 },
{ "timestamp": "2023-04-18 11:00:00", "value": 127.87 },
{ "timestamp": "2023-04-18 12:00:00", "value": 128.53 },
{ "timestamp": "2023-04-18 13:00:00", "value": 129.21 },
{ "timestamp": "2023-04-18 14:00:00", "value": 129.87 },
{ "timestamp": "2023-04-18 15:00:00", "value": 130.34 },
{ "timestamp": "2023-04-18 16:00:00", "value": 131.07 }
]
},
{
"ticker": "GOOG",
"data": [
{ "timestamp": "2023-04-18 09:00:00", "value": 2385.21 },
{ "timestamp": "2023-04-18 10:00:00", "value": 2396.43 },
{ "timestamp": "2023-04-18 11:00:00", "value": 2407.87 },
{ "timestamp": "2023-04-18 12:00:00", "value": 2418.53 },
{ "timestamp": "2023-04-18 13:00:00", "value": 2429.21 },
{ "timestamp": "2023-04-18 14:00:00", "value": 2430.87 },
{ "timestamp": "2023-04-18 15:00:00", "value": 2441.34 },
{ "timestamp": "2023-04-18 16:00:00", "value": 2451.07 }
]
},
{
"ticker": "TSLA",
"data": [
{ "timestamp": "2023-04-18 09:00:00", "value": 676.21 },
{ "timestamp": "2023-04-18 10:00:00", "value": 678.43 },
{ "timestamp": "2023-04-18 11:00:00", "value": 682.87 },
{ "timestamp": "2023-04-18 12:00:00", "value": 686.53 },
{ "timestamp": "2023-04-18 13:00:00", "value": 688.21 },
{ "timestamp": "2023-04-18 14:00:00", "value": 691.87 },
{ "timestamp": "2023-04-18 15:00:00", "value": 694.34 },
{ "timestamp": "2023-04-18 16:00:00", "value": 697.07 }
]
},
{
"ticker": "AMZN",
"data": [
{ "timestamp": "2023-04-18 09:00:00", "value": 3185.21 },
{ "timestamp": "2023-04-18 10:00:00", "value": 3196.43 },
{ "timestamp": "2023-04-18 11:00:00", "value": 3207.87 },
{ "timestamp": "2023-04-18 12:00:00", "value": 3218.53 },
{ "timestamp": "2023-04-18 13:00:00", "value": 3229.21 },
{ "timestamp": "2023-04-18 14:00:00", "value": 3230.87 },
{ "timestamp": "2023-04-18 15:00:00", "value": 3241.34 },
{ "timestamp": "2023-04-18 16:00:00", "value": 3251.07 }
]
}
]
path {
fill: none;
stroke-width: 2px;
}
svg {
display: block;
border: 1px solid black;
}
.rect-with-border {
stroke: black;
stroke-width: 3px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment