block by sxywu 2b22df50909a1d3359fe647f51e97506

openvis tweets #4

Full Screen

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

forked from sxywu‘s block: openvis tweets #3

index.html

<!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="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet">
  <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 playing = false;
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.date <= nextProps.time,
      });
    });
    
    this.setState({bars});
  }, 
  
  componentDidUpdate() {
    this.axisG.call(this.axis);
  },

  onClick(bar) {
    this.props.onClick(bar.date);
  },
  
  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: [],
      time: startDate,
    };
  },
  
  componentWillMount() {
    d3.json('tweets.json', tweets => {
      tweets = _.chain(tweets)
        .filter(tweet => {
          tweet.date = new Date(tweet.postedTime);
          return !tweet.body.match(/^RT/) &&
            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 tweetsAtTime = [];
      var fifteen = 1000 * 60 * 15;
      var tweetsByTime = _.chain(tweets)
        .groupBy(tweet => {
          tweet.dateRounded = Math.floor(tweet.date.getTime() / fifteen) * fifteen;
          return tweet.dateRounded;
        }).map((tweets, date) => {
          return {
            date: new Date(parseInt(date)),
            tweets: tweets,
          };
        }).value();

      this.setState({tweets, tweetsByTime, tweetsAtTime});
    });
  },
  
  onClickBar(time) {
    this.setState({time});
  	playing = false;
  },

  onClickPlay() {
    playing = true;

    var fifteen = 1000 * 60 * 15;
    var tick = 250;
    var lastTick = 0;
    var time = this.state.time >= endDate ? startDate : this.state.time;
    d3.timer(elapsed => {
      if (elapsed > (lastTick + tick)) {
        lastTick = elapsed;
        time = new Date(time.getTime() + fifteen);
        console.log(time);

        this.setState({time});
      }

      if (time >= endDate || !playing) {
        playing = false;
        this.forceUpdate();
        return true;
      }
    });
  },

  onClickPause() {
    playing = false;
    this.forceUpdate();
  },
  
  render() {
    var headerStyle = {
      padding: padding.top + 'px ' + padding.left + 'px',
      cursor: 'pointer',
    }
    var tweetStyle = {
      padding: '10px',
    };
    var tweets = [];
    _.each(this.state.tweetsByTime, (time, i) => {
      // if tweets are less than current time, then show them
      if (time.date <= this.state.time) {
        _.each(time.tweets, (tweet, j) => {
          var key = i + ',' + j;
          tweets.push((
            <div key={key} style={tweetStyle}>
              <strong>{tweet.actor.displayName}</strong> (<a href={tweet.link} target='_new'>{dateFormat(tweet.date)}</a>)
              <div dangerouslySetInnerHTML={{__html: tweet.body}} />
            </div>
          ));
        });
      }
    });
    var play = (<span className='glyphicon glyphicon-play' onClick={this.onClickPlay} />);
    if (playing) {
      play = (<span className='glyphicon glyphicon-pause' onClick={this.onClickPause} />);
    }

    return (
      <div>
        <h2 style={headerStyle}>
          {play} {dateFormat(this.state.time)}
        </h2>
        <Histogram {...this.state} onClick={this.onClickBar} />
        <div style={tweetStyle}>
          {tweets}
        </div>
      </div>
    );
  },
});

ReactDOM.render(
  <App />,
  document.getElementById('main') 
);
  </script>
</body>