Spread of tweets during the two days of Openvis Conf.
--
Built with blockbuilder.org
forked from sxywu's block: openvis tweets #1
forked from sxywu's block: openvis tweets #2
Spread of tweets during the two days of Openvis Conf.
--
Built with blockbuilder.org
forked from sxywu's block: openvis tweets #1
forked from sxywu's block: openvis tweets #2
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://fb.me/react-0.14.3.js"></script> | |
<script src="https://fb.me/react-dom-0.14.3.js"></script> | |
<script src="https://npmcdn.com/babel-core@5.8.34/browser.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.11.2/lodash.js'></script> | |
<link href='https://fonts.googleapis.com/css?family=Lora' rel='stylesheet' type='text/css'> | |
<style> | |
body { | |
font-family: 'Lora', serif; | |
margin:0; | |
color: #49438C; | |
} | |
svg { | |
width: 100%; | |
height: 125px; | |
} | |
text { | |
font-size: .8em; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
a { | |
color: #fe2b75; | |
} | |
</style> | |
</head> | |
<body> | |
<div id='main'></div> | |
<script type="text/babel"> | |
var padding = {top: 25, left: 50}; | |
var colors = {purple: '#49438C', pink: '#fe2b75'}; | |
var startDate = new Date('2016-04-25T00:00:00-04:00'); | |
var endDate = new Date('2016-04-27T00:00:00-04:00'); | |
var dateFormat = d3.time.format('%x %I:%M%p'); | |
var Histogram = React.createClass({ | |
getInitialState() { | |
return { | |
width: window.innerWidth - padding.left * 2, | |
height: 75, | |
bars: [], | |
}; | |
}, | |
componentWillMount() { | |
this.timeScale = d3.time.scale() | |
.domain([startDate, endDate]) | |
.range([0, this.state.width]); | |
this.heightScale = d3.scale.linear() | |
.range([5, this.state.height]); | |
this.axis = d3.svg.axis() | |
.orient('bottom') | |
.ticks(d3.time.hours, 6) | |
.scale(this.timeScale); | |
}, | |
componentDidMount() { | |
this.axisG = d3.select(this.refs.axis); | |
}, | |
componentWillReceiveProps(nextProps) { | |
var minHeight = d3.min(nextProps.tweetsByTime, time => time.tweets.length); | |
var maxHeight = d3.max(nextProps.tweetsByTime, time => time.tweets.length); | |
this.heightScale.domain([minHeight, maxHeight]); | |
var bars = []; | |
_.each(nextProps.tweetsByTime, (time) => { | |
bars.push({ | |
data: time, | |
x: Math.floor(this.timeScale(time.date) * 100) / 100, | |
height: Math.floor(this.heightScale(time.tweets.length) * 100) / 100, | |
selected: time === nextProps.selectedTime, | |
}); | |
}); | |
this.setState({bars}); | |
}, | |
componentDidUpdate() { | |
this.axisG.call(this.axis); | |
}, | |
onClick(bar) { | |
this.props.onClick(bar); | |
}, | |
render() { | |
var barWidth = Math.floor(this.state.width / this.state.bars.length); | |
var bars = _.map(this.state.bars, (bar, i) => { | |
var x = bar.x - (barWidth / 2); | |
var y = this.state.height - bar.height; | |
var barStyle = { | |
fill: bar.selected ? colors['pink'] : colors['purple'], | |
fillOpacity: 0.5, | |
cursor: 'pointer', | |
} | |
return (<rect key={i} x={x} y={y} width={barWidth} height={bar.height} | |
style={barStyle} onClick={this.onClick.bind(this, bar.data)} />); | |
}); | |
var axis = (<g className='axis' ref='axis' | |
transform={'translate(0,' + this.state.height + ')'} />); | |
return ( | |
<svg> | |
<g transform={'translate(' + padding.left + ',' + padding.top + ')'}> | |
{bars} | |
{axis} | |
</g> | |
</svg> | |
); | |
} | |
}); | |
var App = React.createClass({ | |
getInitialState() { | |
return { | |
tweets: {}, | |
tweetsByTime: [], | |
selectedTime: {}, | |
}; | |
}, | |
componentWillMount() { | |
d3.json('tweets.json', tweets => { | |
tweets = _.chain(tweets) | |
.filter(tweet => { | |
tweet.date = new Date(tweet.postedTime); | |
return startDate <= tweet.date && tweet.date <= endDate; | |
}).sortBy(tweet => tweet.date) | |
.reduce((obj, tweet) => { | |
obj[tweet.link] = tweet; | |
return obj; | |
}, {}) | |
.value(); | |
// group by every 5min | |
var coeff = 1000 * 60 * 15; | |
var tweetsByTime = _.chain(tweets) | |
.groupBy(tweet => { | |
tweet.dateRounded = Math.floor(tweet.date.getTime() / coeff) * coeff; | |
return tweet.dateRounded; | |
}).map((tweets, time) => { | |
return { | |
date: new Date(parseInt(time)), | |
tweets: tweets, | |
} | |
}).value(); | |
this.setState({tweets, tweetsByTime}); | |
}); | |
}, | |
onClickBar(selectedTime) { | |
this.setState({selectedTime}); | |
}, | |
render() { | |
var tweetStyle = { | |
padding: '10px', | |
}; | |
var tweets = _.map(this.state.selectedTime.tweets, (tweet, i) => { | |
return ( | |
<div key={i} style={tweetStyle}> | |
<strong>{tweet.actor.displayName}</strong> (<a href={tweet.link} target='_new'>{dateFormat(tweet.date)}</a>) | |
<div dangerouslySetInnerHTML={{__html: tweet.body}} /> | |
</div> | |
); | |
}); | |
return ( | |
<div> | |
<div style={{padding: padding.left}}> | |
Tweets with #openvisconf or @openvisconf. Click on bars to see tweets from that time block. | |
</div> | |
<Histogram {...this.state} onClick={this.onClickBar} /> | |
<div style={tweetStyle}> | |
{tweets} | |
</div> | |
</div> | |
); | |
}, | |
}); | |
ReactDOM.render( | |
<App />, | |
document.getElementById('main') | |
); | |
</script> | |
</body> |