var StackedAreaChart = React.createClass({ mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin], numElements: 10, maxWeeks: 30, height: 350, xAxisHeight: 20, words: { items: { repo: 'repositories', team: 'teams', user: 'contributors' }, item: { repo: 'repository', team: 'team' }, actions: { repo: 'which were the most attended by', team: 'which were the most active working on', user: 'which were the most active working on' } }, getInitialState: function() { return { item: this.props.items[0], rawData: [], top: [], weeks: [], max: 1 }; }, componentDidMount: function() { this.calculateViewBoxWidth(); window.addEventListener('resize', this.calculateViewBoxWidth); }, componentWillReceiveProps: function(newProps) { this.setState({ item: (_.isEqual(newProps.items, this.props.items) ? this.state.item : newProps.items[0]), state: 'newProps' }, this.fetchData); }, shouldComponentUpdate: function(newProps, newState) { if (!newState.canvasWidth) { return false; } if (newState.state !== 'newPoints') { return false; } return true; }, handleFilter: function(thing, i) { if (this.props.items[i] !== this.state.item) { this.setState({ item: this.props.items[i], state: 'newProps' }, this.fetchData); } }, handleClick: function(item) { var params = {org: this.getParams().org}; params[this.state.item] = item; this.transitionTo(this.state.item, params, this.getQuery()); }, handleFocusIn: function(i) { var node = this.refs.container.getDOMNode(); node.className = 'sachart-container focused item-'+ i; }, handleFocusOut: function() { var node = this.refs.container.getDOMNode(); node.className = 'sachart-container'; }, handleNewData: function() { // Group commits by items var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value(); if (weeksList.length < 2) { this.setState({ weeks: [], state: 'newPoints' }); return; } var counts = _.reduce(this.state.rawData, function(res, el) { if (weeksList.indexOf(el.week) === -1) { return res; } if (res[el.item] === undefined) { res[el.item] = el.commits; } else { res[el.item] += el.commits; } return res; }, {}); // Extract top items from var top = _(_.pairs(counts)) // Take [item, count] pairs from counts object .sortBy(1).reverse() // sort them by count (descending) .take(this.numElements) // take first N pairs .pluck(0) // keep only items, omit the counts .value(); for (var i = top.length; i < this.numElements; i++) { top[i] = null; }; var weeks = _.reduce(this.state.rawData, function(res, el) { if (weeksList.indexOf(el.week) === -1) { return res; } if (res[el.week] === undefined) { res[el.week] = {}; } if (top.indexOf(el.item) > -1) { res[el.week][el.item] = el.commits; } return res; }, {}); var max = _.max(_.map(weeksList, function(week){ return _.sum(_.values(weeks[week])); })); this.setState({ top: top, weeks: weeks, max: max, state: 'newPoints' }); }, buildPathD: function(points) { var maxWidth = this.state.canvasWidth, maxHeight = this.height; var dots = this.buildDots(points); var first = dots.shift(); var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; }); d.unshift('M'+ first.x +','+ first.y); d.push('L'+ maxWidth +','+ maxHeight); d.push('L0,'+ maxHeight +' Z'); return d.join(' '); }, buildDots: function(points) { var maxWidth = this.state.canvasWidth, maxHeight = this.height, maxValue = this.state.max, len = points.length; return _.map(points, function(point, i) { point.x = i/(len-1)*maxWidth; point.y = maxHeight - point.point; return point; }); }, render: function() { var maxWidth = this.state.canvasWidth, maxHeight = this.height, top = this.state.top, max = this.state.max; // [week, [{val, point}, ...]] var points = _(this.state.weeks) .map(function(items, week) { var values = _.map(top, function(item) { return items[item] || 0; }); var sum = 0; var points = _.map(values, function(val) { sum += val/max*maxHeight*0.96; return { val: val, point: sum }; }); return [parseInt(week, 10), points]; }) .sort(0) .reverse() .take(this.maxWeeks) .reverse() .value(); // [item, [{val, point}, ...]] var paths = _.map(top, function(item, i) { var itemPoints = _.map(points, function(pair) { return pair[1][i]; }); return[item, itemPoints]; }); var areas = _.map(paths, function(pair, i) { var item = pair[0], path = pair[1]; return ( ); }.bind(this)); var words = this.words, who = this.getParams().repo || this.getParams().team || this.getParams().user || this.getParams().org; var params = Object.keys(this.getParams()); params.splice(params.indexOf('org'), 1); var subject = params[0]; var renderDot = function(item, i, dot, j) { if (dot.val === 0) { return null; } var maxWidth = this.state.canvasWidth, maxHeight = this.height, radius = 10, x = dot.x, y = dot.y; if (x < radius) { x = radius } else if (x > maxWidth - radius) { x = maxWidth - radius; } if (y < radius) { y = radius; } else if (y > maxHeight - radius) { y = maxHeight - radius; } return ( ); }.bind(this); var renderedDots = _.map(paths, function(pair, i) { var item = pair[0], path = pair[1]; var dots = this.buildDots(path); return dots.map(renderDot.bind(this, item, i)); }.bind(this)); var renderLegend = function(item, i){ return (
  • {item}
  • ); }.bind(this); var legend = _(paths).pluck(0).filter(function(el){ return el !== null; }).value(); return (
    This stacked area chart represents {words.items[this.state.item]} {words.actions[this.state.item]} {who} {words.item[subject]}
    {areas.reverse()} {renderedDots}
      {legend.map(renderLegend)}
    ); } }); var StackedArea = React.createClass({ mixins: [ChartAnimationMixin], getInitialState: function() { return {}; }, componentWillReceiveProps: function(newProps) { this.setState({ lastd: this.props.d || newProps.d, }, this.animateAll); }, animateAll: function() { this.clearAnimations(this.refs.path); this.animate(this.refs.path, 'd', this.state.lastd, this.props.d); }, render: function() { return ( ); } }); var Dot = React.createClass({ mixins: [ChartAnimationMixin], radius: 10, getInitialState: function() { return {}; }, componentWillReceiveProps: function(newProps) { this.setState({ lastY: this.props.y || newProps.y }, this.animateAll); }, animateAll: function() { this.clearAnimations(this.refs.dot); this.animate(this.refs.dot, 'cy', this.state.lastY, this.props.y); }, render: function() { return ( {this.props.value} ); } }); var Axis = React.createClass({ render: function() { if (this.props.weeks.length === 0) { return null; } var renderMark = function(week, i) { var x = i/(this.props.weeks.length - 1)*this.props.width, ta = (i === 0 ? 'start' : (i === this.props.weeks.length - 1 ? 'end' : 'middle')); return ( {formatDate(week)} ); }.bind(this); return ( {this.props.weeks.map(renderMark)} ) } });