Skip to content

Instantly share code, notes, and snippets.

@josephcc
Last active March 13, 2018 06:29
Show Gist options
  • Save josephcc/64fcd9f4492b6f701b8a2ed5380a7e2a to your computer and use it in GitHub Desktop.
Save josephcc/64fcd9f4492b6f701b8a2ed5380a7e2a to your computer and use it in GitHub Desktop.
import React from 'react'
import ReactDOM from 'react-dom'
import { sortBy, last, sortedIndexBy } from 'lodash'
import { RadioGroup, RadioButton } from 'react-radio-buttons'
let d3 = Object.assign({},
require('d3-shape'),
require('d3-scale'),
require('d3-axis'),
require('d3-selection'),
require('d3-array'),
require('d3-ease'),
require('d3-hierarchy'),
require('d3-color'),
require('d3-fetch'),
require('d3-transition'),
require('d3-time-format'),
)
export default class Homework extends React.Component {
static defaultProps = {
width: 1200,
height: 600,
margin: {top: 20, right: 10, bottom: 50, left: 50},
transitionDuration: 1000,
fontSize: 10,
yDomain: [19, 61],
xDomain: [new Date(2017, 3, 15), new Date(2018, 10, 6)],
csvUrl: 'https://projects.fivethirtyeight.com/generic-ballot-data/generic_topline.csv',
csvUrl2: 'https://projects.fivethirtyeight.com/generic-ballot-data/generic_polllist.csv',
subgroups: ['All polls', 'Voters', 'Adults'],
demColor: d3.hsl('#008ED5'),
repColor: d3.hsl('#FF2701')
}
constructor(props) {
super(props)
this.state = {
data: [],
data2: [],
subgroup: this.props.subgroups[0]
}
d3.csv(this.props.csvUrl, (row) => {
row.date = d3.timeParse('%m/%e/%Y')(row.modeldate)
return row
}).then((data) => {
this.setState({data: data})
this.update()
})
d3.csv(this.props.csvUrl2, (row) => {
row.date = d3.timeParse('%m/%e/%Y')(row.enddate)
row.samplesize = parseInt(row.samplesize)
return row
}).then((data) => {
this.setState({data2: data})
this.update()
})
}
_dynamicLine(_day) {
if (_day === undefined) {
_day = last(this.data)
}
this.day
.attr('transform', `translate(${this.xScale(_day.date)}, 0)`)
this.day.select('.date')
.text(d3.timeFormat('%b %d, %Y')(_day.date))
this.day.select('.demNum')
.attr('y', this.yScale(_day.dem_estimate))
.text(`${Math.round(_day.dem_estimate)}`)
.append('tspan')
.attr('fill', 'black')
.attr('stroke-width', 0)
.attr('font-size', 14)
.attr('font-weight', 'bold')
.attr('dy', -12)
.text('%')
.append('tspan')
.attr('fill', 'black')
.attr('stroke-width', 0)
.attr('font-size', 14)
.attr('font-weight', 'bold')
.attr('dx', 2)
.text('Democrats')
this.day.select('.repNum')
.attr('y', this.yScale(_day.rep_estimate))
.text(`${Math.round(_day.rep_estimate)}`)
.append('tspan')
.attr('fill', 'black')
.attr('stroke-width', 0)
.attr('font-size', 14)
.attr('font-weight', 'bold')
.attr('dy', -12)
.text('%')
.append('tspan')
.attr('fill', 'black')
.attr('stroke-width', 0)
.attr('font-size', 14)
.attr('font-weight', 'bold')
.attr('dx', 2)
.text('Republicans')
}
mousemove(a, b, c) {
let x = this.xScale.invert(d3.mouse(c[0])[0] - this.props.margin.left)
let idx = sortedIndexBy(this.data, {date: x}, (d) => d.date)
idx = Math.min(this.data.length - 1, idx)
this._dynamicLine(this.data[idx])
}
initD3(element) {
let width = this.props.width - this.props.margin.left - this.props.margin.right
let height = this.props.height - this.props.margin.top - this.props.margin.bottom
d3.select(element).select('svg').remove()
this.canvas = d3.select(element)
.append('svg')
.attr('width', this.props.width)
.attr('height', this.props.height)
.on('mousemove', this.mousemove.bind(this))
.on('mouseleave', this._dynamicLine.bind(this))
.append('g')
.attr('transform', `translate(${this.props.margin.left}, ${this.props.margin.top})`)
this.yScale = d3
.scaleLinear()
.range([height, 0])
.domain(this.props.yDomain)
let yAxis = d3
.axisLeft(this.yScale)
.tickSize(-width)
.ticks(5)
.tickFormat((t, idx) => (idx === 4 ? `${t}%` : `${t}`))
this.xScale = d3
.scaleTime()
.range([0, width])
.domain(this.props.xDomain)
this.rxScale = d3
.scaleTime()
.domain([0, width])
.range(this.props.xDomain)
let xAxis = d3
.axisBottom(this.xScale)
.ticks(19)
.tickSize(-height)
.tickFormat((time, idx) => {
if (idx === 0 || time.getMonth() === 0) {
return d3.timeFormat('%b %Y')(time)
}
return d3.timeFormat('%b')(time)
})
xAxis = this.canvas.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis)
yAxis = this.canvas.append('g')
.call(yAxis)
let dday = this.canvas
.append('g')
.attr('transform', `translate(${this.xScale(last(this.props.xDomain))}, 0)`)
dday
.append('line')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', height)
dday
.append('text')
.attr('x', -4).attr('y', this.yScale(58))
.attr('text-anchor', 'end')
.attr('font-weight', 'bold')
.text('Election Day')
dday
.append('text')
.attr('x', -4).attr('y', this.yScale(58) + 16)
.attr('text-anchor', 'end')
.text('NOV. 6, 2018')
xAxis
.selectAll('text')
.style('text-anchor', 'middle')
.attr('font-size', '1.4em')
.attr('fill', 'gray')
.attr('dx', '0em')
.attr('dy', '1em')
yAxis
.selectAll('text')
.style('text-anchor', 'start')
.attr('font-size', '1.4em')
.attr('fill', 'gray')
.attr('dx', '-2em')
xAxis
.selectAll('line')
.attr('stroke', 'lightgray')
yAxis
.selectAll('line')
.attr('stroke', 'lightgray')
xAxis
.selectAll('path')
.attr('stroke-width', 0)
yAxis
.selectAll('path')
.attr('stroke-width', 0)
}
_plotArea(data, column0, column1, color, duration, delay) {
data = sortBy(data, 'date')
let canvas = this.canvas.append('g')
let area = canvas.selectAll('.area')
.data([data])
area
.enter().append('path').classed('area', true)
.merge(area)
.attr('stroke-width', 3)
.style('fill', '#0000')
.attr('d', d3.area()
.x((d) => this.xScale(d.date))
.y0((d) => this.yScale(d[column1]))
.y1((d) => this.yScale(d[column0]))
)
canvas.selectAll('.area').transition().duration(duration).delay(delay).ease(d3.easeLinear)
.style('fill', color)
area.exit().remove()
}
_plotLine(data, column, color, duration, delay) {
data = sortBy(data, 'date')
let canvas = this.canvas.append('g')
let line = canvas.selectAll('.line')
.data([data])
line
.enter().append('path').classed('line', true)
.merge(line)
.style('fill', 'none')
.attr('stroke', color)
.attr('stroke-width', 3)
.attr('stroke-dasharray', 2000)
.attr('stroke-dashoffset', 2000)
.attr('d', d3.line()
.x((d) => this.xScale(d.date))
.y((d) => this.yScale(d[column]))
)
canvas.selectAll('.line').transition().duration(duration).delay(delay).ease(d3.easeLinear)
.attr('stroke-dashoffset', 0)
line.exit().remove()
}
_plotDots(data, column, color, duration, delay) {
data = sortBy(data, 'date')
let canvas = this.canvas.append('g')
let dots = canvas.selectAll('.dots')
.data(data)
dots = dots
.enter().append('circle')
.merge(dots)
.attr('class', 'dots')
.attr('cx', (d) => this.xScale(d.date))
.attr('cy', (d) => this.yScale(d[column]))
.attr('stroke', 'white')
.attr('stroke-width', 0.5)
.attr('r', 0)
.attr('fill', color)
dots.transition().duration(duration).delay(delay).ease(d3.easeLinear)
.attr('r', (d) => this.rScale(d.samplesize))
dots.exit().remove()
}
_plotLegend() {
if (this.legend !== undefined)
this.legend.remove()
let steps = [1000, 2000, 3000, 4000]
let height = 80
this.legend = this.canvas.append('g')
.attr('transform', `translate(${this.props.width - 160}, ${this.props.height - 120 - this.props.margin.top - this.props.margin.bottom})`)
this.legend.append('text')
.attr('x', 0)
.attr('y', 10)
.attr('fill', '#555')
.text('Sample Size')
let legend2 = this.legend.append('g')
.attr('transform', `translate(0, 24)`)
.selectAll('.dot')
.data(steps)
legend2 = legend2
.enter().append('circle')
.merge(legend2)
.attr('class', 'dot')
.attr('cy', (d, i) => i*(height/steps.length))
.attr('cx', 5)
.attr('fill', '#555')
.attr('stroke-width', 0)
.attr('r', (d) => this.rScale(d))
let legend3 = this.legend.append('g')
.attr('transform', `translate(0, 24)`)
.selectAll('.text')
.data(steps)
legend3 = legend3
.enter().append('text')
.merge(legend3)
.attr('class', 'text')
.attr('y', (d, i) => i*(height/steps.length) + 6)
.attr('x', 5 + 10)
.attr('fill', '#555')
.attr('stroke-width', 0)
.text((d) => d)
}
update() {
let data = this.state.data
let data2 = this.state.data2
if (data.length === 0 || data2.length === 0) {
return
}
this.data = data.filter((d) => d.subgroup === this.state.subgroup)
this.data = sortBy(this.data, 'date')
this.data2 = data2.filter((d) => d.subgroup === this.state.subgroup)
let _sampleSizes = this.state.data2.map((d) => d.samplesize)
this.rScale = d3
.scaleSqrt()
.range([1, 5])
.domain([Math.min(..._sampleSizes), Math.max(..._sampleSizes)])
this._plotLegend()
this.props.demColor.opacity = 0.1
this.props.repColor.opacity = 0.1
this._plotArea(this.data, 'dem_hi', 'dem_lo', this.props.demColor, 500, 750)
this._plotArea(this.data, 'rep_hi', 'rep_lo', this.props.repColor, 500, 750)
this.props.demColor.opacity = 0.25
this.props.repColor.opacity = 0.25
this._plotDots(this.data2, 'dem', this.props.demColor, 750, 0)
this._plotDots(this.data2, 'rep', this.props.repColor, 750, 0)
this.props.demColor.opacity = 1.0
this.props.repColor.opacity = 1.0
this._plotLine(this.data, 'dem_estimate', '#008ED5', 1500, 1250)
this._plotLine(this.data, 'rep_estimate', '#FF2701', 1500, 1250)
if (this.day !== undefined) {
this.day.remove()
}
this.day = this.canvas
.append('g')
this.day
.append('line')
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '4, 2')
.attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', this.props.height - this.props.margin.left - this.props.margin.right)
this.day
.append('text')
.attr('class', 'date')
.attr('text-anchor', 'start')
.attr('font-weight', 'bold')
.attr('x', 4).attr('y', this.yScale(58) + 2)
this.day
.append('text')
.attr('class', 'demNum')
.attr('fill', this.props.demColor)
.attr('x', 4)
.attr('font-size', 40)
.attr('font-weight', 'bold')
.attr('stroke', 'white')
.attr('width', 1)
this.day
.append('text')
.attr('class', 'repNum')
.attr('fill', this.props.repColor)
.attr('x', 4)
.attr('font-size', 40)
.attr('font-weight', 'bold')
.attr('stroke', 'white')
.attr('width', 1)
this._dynamicLine(last(this.data))
}
componentDidUpdate() {
this.initD3(ReactDOM.findDOMNode(this.refs.homework))
this.update()
}
componentDidMount() {
this.initD3(ReactDOM.findDOMNode(this.refs.homework))
this.update()
}
render() {
let sourceUrl = 'https://gist.github.com/64fcd9f4492b6f701b8a2ed5380a7e2a'
return (
<div style={{padding: '20px'}}>
<h2>Are Democrats/Republicans Winning The Race For Congress?</h2>
<h5>
Basically a clone of <a target='_blank' href='https://projects.fivethirtyeight.com/congress-generic-ballot-polls/'>this</a> with some extra features. Mouse over to see past numbers. Written in D3.js.
</h5>
<div className='homework' ref='homework' style={{}}/>
<div style={{width: '800px'}}>
<RadioGroup onChange={ (subgroup) => this.setState({subgroup: subgroup}) } value={this.state.subgroup} horizontal>
{ this.props.subgroups.map((subgroup) => {
return (
<RadioButton value={subgroup} iconSize={20} key={subgroup}>
{subgroup}
</RadioButton>
)
})}
</RadioGroup>
</div>
<h5>
Source code: <a target='_blank' href={sourceUrl}>{sourceUrl}</a>
</h5>
</div>)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment