👉 exercise2/js/Container/index.js
Let's take a look at some data.
fakeFetchVote()
.then(voteData => console.log(voteData));
fakeFetchCensus()
.then(censusData => console.log(censusData));
👉 exercise2/js/Chart/index.js
Add some getter-setters to our chart to handle both data sets.
voteData(arr) {
if (!arr) return this._voteData;
this._voteData = arr;
return this;
}
censusData(obj) {
if (!obj) return this._censusData;
this._censusData = obj;
return this;
}
Fill in the draw func with pure, sweet chart:
const margin = 60;
const node = this.selection().node();
const { width } = node.getBoundingClientRect();
const height = width;
const t = d3.transition()
.duration(750);
const voteData = this.voteData();
const censusData = this.censusData();
const nonWhitePercent = Object.keys(censusData).map(fips => censusData[fips]);
const x = d3.scaleLinear()
.domain([1.1, -0.1]) // plus a little offset
.range([0, width - margin]);
const y = d3.scaleLinear()
.domain([
d3.min(nonWhitePercent) - 0.1,
d3.max(nonWhitePercent) + 0.1,
])
.range([height - margin, 0]);
const g = this.selection()
.appendSelect('svg')
.attr('width', width)
.attr('height', height)
.appendSelect('g')
.attr('transform', 'translate(' + margin / 2 + ', ' + margin / 2 + ')');
g.appendSelect('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width - margin)
.attr('height', height - margin)
.style('fill', '#eee');
const dots = g.selectAll('.dot')
.data(voteData, d => d.fips);
dots.enter().append('circle')
.attr('class', 'dot')
.style('stroke-width', '1px')
.style('fill-opacity', 0.3)
.attr('r', 5)
.attr('cy', d => y(censusData[d.fips]))
.attr('cx', d => x(d.dem))
.merge(dots)
.style('fill', d => d.dem > 0.5 ? '#3571C0' : '#FE5C40')
.style('stroke', d => d.dem > 0.5 ? '#3571C0' : '#FE5C40')
.transition(t)
.attr('cy', d => y(censusData[d.fips]))
.attr('cx', d => x(d.dem));
const forLinReg = voteData.map((d, i) => [d.dem, censusData[d.fips]]);
// calculate linear regression and correlations
const linReg = stats.linearRegression(forLinReg);
// Draw line of best fit
let x1 = d3.min(voteData, d => d.dem);
let x2 = d3.max(voteData, d => d.dem);
let y1 = d3.min(voteData, d => d.dem) * linReg.m + linReg.b;
let y2 = d3.max(voteData, d => d.dem) * linReg.m + linReg.b;
const yMin = y.domain()[0];
const yMax = y.domain()[1];
// Check y overflow for positive slope
if (linReg.m > 0) {
if (y1 < yMin) {
x1 = (yMin - linReg.b) / linReg.m;
y1 = yMin;
}
if (y2 > yMax) {
x2 = (yMax - linReg.b) / linReg.m;
y2 = yMax;
}
// for negative slope
} else {
if (y1 > yMax) {
x1 = (yMax - linReg.b) / linReg.m;
y1 = yMax;
}
if (y2 < yMin) {
x2 = (yMin - linReg.b) / linReg.m;
y2 = yMin;
}
}
g.appendSelect('line', 'linReg')
.style('stroke', 'black')
.style('stroke-width', 3)
.transition(t)
.attr('x1', x(x1))
.attr('x2', x(x2))
.attr('y1', y(y1))
.attr('y2', y(y2));
g.appendSelect('text', 'axis dem')
.attr('x', 0)
.attr('y', height - margin / 2 - 15)
.text('← More Dem');
g.appendSelect('text', 'axis gop')
.attr('x', width - margin)
.attr('y', height - margin / 2 - 15)
.text('More GOP →')
.style('text-anchor', 'end');
g.appendSelect('text', 'axis max')
.attr('x', -100)
.attr('y', -5)
.attr('transform', 'rotate(-90, 0, 0)')
.text('Least white →');
👉 exercise2/js/Container/index.js
Add our chart to our component:
chart = new Chart();
Let's create some state to hold our data:
state = {
censusData: null,
voteData: null,
}
componentDidMount() {
fakeFetchVote()
.then(voteData => this.setState({ voteData }));
fakeFetchCensus()
.then(censusData => this.setState({ censusData }));
}
But where do we call our chart when the fetches will return data asynchronously?
Put it in componentDidUpdate
!
componentDidUpdate() {
this.chart
.selection('#chart')
.voteData(this.state.voteData)
.censusData(this.state.censusData)
.draw();
}
render() {
return (
<div id='chart' />
);
}
Add a conditional so we don't draw until we have both datasets:
if (!this.state.censusData || !this.state.voteData) return;
Add a recurring fetch for our vote data:
// Fetch vote data every 5 seconds
setInterval(() => {
fakeFetchVote()
.then(voteData => this.setState({ voteData }));
}, 5000);