|
/* 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(); |