2015-03-11 12:10:11 +00:00
|
|
|
var StackedAreaChart = React.createClass({
|
2015-03-13 12:08:13 +00:00
|
|
|
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
|
2015-03-11 12:10:11 +00:00
|
|
|
|
|
|
|
numElements: 10,
|
2015-03-15 12:10:02 +00:00
|
|
|
maxWeeks: 30,
|
2015-03-14 15:53:57 +00:00
|
|
|
height: 350,
|
2015-03-16 12:05:39 +00:00
|
|
|
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'
|
|
|
|
}
|
|
|
|
},
|
2015-03-11 12:10:11 +00:00
|
|
|
|
|
|
|
getInitialState: function() {
|
|
|
|
return {
|
|
|
|
item: this.props.items[0],
|
|
|
|
rawData: [],
|
|
|
|
top: [],
|
|
|
|
weeks: [],
|
2015-03-13 12:08:13 +00:00
|
|
|
max: 1
|
2015-03-11 12:10:11 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2015-03-11 20:21:28 +00:00
|
|
|
componentDidMount: function() {
|
|
|
|
this.calculateViewBoxWidth();
|
|
|
|
window.addEventListener('resize', this.calculateViewBoxWidth);
|
|
|
|
},
|
|
|
|
|
2015-03-11 20:18:14 +00:00
|
|
|
componentWillReceiveProps: function(newProps) {
|
|
|
|
this.setState({
|
2015-03-16 16:04:10 +00:00
|
|
|
item: (_.isEqual(newProps.items, this.props.items)
|
|
|
|
? this.state.item
|
|
|
|
: newProps.items[0]),
|
2015-03-12 13:11:52 +00:00
|
|
|
state: 'newProps'
|
2015-03-11 20:18:14 +00:00
|
|
|
}, this.fetchData);
|
|
|
|
},
|
|
|
|
|
2015-03-12 13:11:52 +00:00
|
|
|
shouldComponentUpdate: function(newProps, newState) {
|
2015-03-13 12:08:13 +00:00
|
|
|
if (!newState.canvasWidth) {
|
2015-03-12 13:11:52 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (newState.state !== 'newPoints') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2015-03-11 12:10:11 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
handleFilter: function(thing, i) {
|
|
|
|
if (this.props.items[i] !== this.state.item) {
|
|
|
|
this.setState({
|
2015-03-12 13:11:52 +00:00
|
|
|
item: this.props.items[i],
|
|
|
|
state: 'newProps'
|
2015-03-11 12:10:11 +00:00
|
|
|
}, this.fetchData);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-16 17:17:04 +00:00
|
|
|
handleClick: function(item) {
|
2015-03-11 12:10:11 +00:00
|
|
|
var params = {org: this.getParams().org};
|
2015-03-16 17:17:04 +00:00
|
|
|
params[this.state.item] = item;
|
|
|
|
this.transitionTo(this.state.item, params, this.getQuery());
|
2015-03-11 12:10:11 +00:00
|
|
|
},
|
|
|
|
|
2015-03-16 12:47:39 +00:00
|
|
|
handleFocusIn: function(i) {
|
|
|
|
var node = this.refs.container.getDOMNode();
|
|
|
|
node.className = 'sachart-container focused item-'+ i;
|
|
|
|
},
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
handleFocusOut: function() {
|
2015-03-16 12:47:39 +00:00
|
|
|
var node = this.refs.container.getDOMNode();
|
|
|
|
node.className = 'sachart-container';
|
|
|
|
},
|
|
|
|
|
2015-03-13 12:08:13 +00:00
|
|
|
handleNewData: function() {
|
2015-03-11 12:10:11 +00:00
|
|
|
// Group commits by items
|
2015-03-16 16:53:54 +00:00
|
|
|
var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value();
|
2015-03-16 17:17:04 +00:00
|
|
|
if (weeksList.length < 2) {
|
|
|
|
this.setState({
|
|
|
|
weeks: [],
|
|
|
|
state: 'newPoints'
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2015-03-12 13:45:29 +00:00
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
var counts = _.reduce(this.state.rawData, function(res, el) {
|
2015-03-12 13:45:29 +00:00
|
|
|
if (weeksList.indexOf(el.week) === -1) {
|
|
|
|
return res;
|
|
|
|
}
|
2015-03-11 12:10:11 +00:00
|
|
|
if (res[el.item] === undefined) {
|
|
|
|
res[el.item] = el.commits;
|
|
|
|
} else {
|
|
|
|
res[el.item] += el.commits;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
// Extract top items from
|
2015-03-16 16:53:54 +00:00
|
|
|
var top = _(_.pairs(counts)) // Take [item, count] pairs from counts object
|
2015-03-11 12:10:11 +00:00
|
|
|
.sortBy(1).reverse() // sort them by count (descending)
|
|
|
|
.take(this.numElements) // take first N pairs
|
|
|
|
.pluck(0) // keep only items, omit the counts
|
|
|
|
.value();
|
2015-03-12 13:45:29 +00:00
|
|
|
for (var i = top.length; i < this.numElements; i++) {
|
|
|
|
top[i] = null;
|
|
|
|
};
|
2015-03-11 12:10:11 +00:00
|
|
|
|
|
|
|
var weeks = _.reduce(this.state.rawData, function(res, el) {
|
2015-03-12 13:45:29 +00:00
|
|
|
if (weeksList.indexOf(el.week) === -1) {
|
|
|
|
return res;
|
|
|
|
}
|
2015-03-11 12:10:11 +00:00
|
|
|
if (res[el.week] === undefined) {
|
|
|
|
res[el.week] = {};
|
|
|
|
}
|
2015-03-11 20:18:14 +00:00
|
|
|
if (top.indexOf(el.item) > -1) {
|
|
|
|
res[el.week][el.item] = el.commits;
|
|
|
|
}
|
2015-03-11 12:10:11 +00:00
|
|
|
return res;
|
|
|
|
}, {});
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
var max = _.max(_.map(weeksList, function(week){ return _.sum(_.values(weeks[week])); }));
|
2015-03-12 13:45:29 +00:00
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
this.setState({
|
2015-03-16 09:24:13 +00:00
|
|
|
top: top,
|
2015-03-12 13:11:52 +00:00
|
|
|
weeks: weeks,
|
2015-03-13 12:08:13 +00:00
|
|
|
max: max,
|
2015-03-12 13:11:52 +00:00
|
|
|
state: 'newPoints'
|
2015-03-11 12:10:11 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
buildPathD: function(points) {
|
|
|
|
var maxWidth = this.state.canvasWidth,
|
2015-03-16 12:05:39 +00:00
|
|
|
maxHeight = this.height;
|
2015-03-13 12:08:13 +00:00
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
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');
|
2015-03-11 12:10:11 +00:00
|
|
|
|
|
|
|
return d.join(' ');
|
|
|
|
},
|
|
|
|
|
2015-03-16 12:05:39 +00:00
|
|
|
buildDots: function(points) {
|
|
|
|
var maxWidth = this.state.canvasWidth,
|
|
|
|
maxHeight = this.height,
|
|
|
|
maxValue = this.state.max,
|
|
|
|
len = points.length;
|
|
|
|
|
|
|
|
return _.map(points, function(point, i) {
|
2015-03-16 16:53:54 +00:00
|
|
|
point.x = i/(len-1)*maxWidth;
|
|
|
|
point.y = maxHeight - point.point;
|
|
|
|
return point;
|
2015-03-16 12:05:39 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
render: function() {
|
|
|
|
var maxWidth = this.state.canvasWidth,
|
|
|
|
maxHeight = this.height,
|
2015-03-16 09:24:13 +00:00
|
|
|
top = this.state.top,
|
2015-03-11 12:10:11 +00:00
|
|
|
max = this.state.max;
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
// [week, [{val, point}, ...]]
|
|
|
|
var points = _(this.state.weeks)
|
2015-03-11 12:10:11 +00:00
|
|
|
.map(function(items, week) {
|
2015-03-16 09:24:13 +00:00
|
|
|
var values = _.map(top, function(item) {
|
2015-03-11 12:10:11 +00:00
|
|
|
return items[item] || 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
var sum = 0;
|
|
|
|
var points = _.map(values, function(val) {
|
2015-03-16 16:53:54 +00:00
|
|
|
sum += val/max*maxHeight*0.96;
|
|
|
|
return {
|
|
|
|
val: val,
|
|
|
|
point: sum
|
|
|
|
};
|
2015-03-11 12:10:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return [week, points];
|
|
|
|
})
|
|
|
|
.sort(0)
|
2015-03-12 13:11:52 +00:00
|
|
|
.reverse()
|
|
|
|
.take(this.maxWeeks)
|
|
|
|
.reverse()
|
2015-03-11 12:10:11 +00:00
|
|
|
.value();
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
// [item, [{val, point}, ...]]
|
2015-03-16 09:24:13 +00:00
|
|
|
var paths = _.map(top, function(item, i) {
|
2015-03-12 13:11:52 +00:00
|
|
|
var itemPoints = _.map(points, function(pair) {
|
|
|
|
return pair[1][i];
|
|
|
|
});
|
|
|
|
return[item, itemPoints];
|
|
|
|
});
|
2015-03-11 12:10:11 +00:00
|
|
|
|
2015-03-12 13:11:52 +00:00
|
|
|
var areas = _.map(paths, function(pair, i) {
|
2015-03-16 16:53:54 +00:00
|
|
|
var item = pair[0],
|
|
|
|
path = pair[1];
|
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
return (
|
2015-03-12 13:11:52 +00:00
|
|
|
<StackedArea key={'area-'+ i}
|
2015-03-16 12:47:39 +00:00
|
|
|
item={item} i={i}
|
2015-03-16 16:53:54 +00:00
|
|
|
d={this.buildPathD(path)}
|
2015-03-16 12:47:39 +00:00
|
|
|
color={Colors[i]}
|
2015-03-16 16:53:54 +00:00
|
|
|
onMouseOver={this.handleFocusIn.bind(this, i)} />
|
2015-03-11 12:10:11 +00:00
|
|
|
);
|
|
|
|
}.bind(this));
|
|
|
|
|
2015-03-16 12:05:39 +00:00
|
|
|
var words = this.words,
|
2015-03-16 16:53:54 +00:00
|
|
|
who = this.getParams().repo ||
|
|
|
|
this.getParams().team ||
|
|
|
|
this.getParams().user ||
|
|
|
|
this.getParams().org;
|
2015-03-14 15:53:57 +00:00
|
|
|
|
|
|
|
var params = Object.keys(this.getParams());
|
|
|
|
params.splice(params.indexOf('org'), 1);
|
|
|
|
var subject = params[0];
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
var renderDot = function(item, i, dot, j) {
|
|
|
|
if (dot.val === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
2015-03-16 17:17:04 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-03-16 12:47:39 +00:00
|
|
|
return (
|
2015-03-16 16:53:54 +00:00
|
|
|
<Dot key={'dot-'+ i +'-'+ j}
|
|
|
|
item={item} i={i}
|
|
|
|
value={dot.val}
|
2015-03-16 17:17:04 +00:00
|
|
|
x={x}
|
|
|
|
y={y}
|
2015-03-16 16:53:54 +00:00
|
|
|
onMouseOver={this.handleFocusIn.bind(this, i)} />
|
|
|
|
);
|
|
|
|
}.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 (
|
|
|
|
<li key={'legend-'+ item}
|
2015-03-16 12:47:39 +00:00
|
|
|
className={'label label-'+ i}
|
|
|
|
onMouseOver={this.handleFocusIn.bind(this, i)}
|
2015-03-16 17:17:04 +00:00
|
|
|
onMouseOut={this.handleFocusOut.bind(this, i)}
|
|
|
|
onClick={this.handleClick.bind(this, item)}
|
|
|
|
>
|
2015-03-16 16:53:54 +00:00
|
|
|
<div className="color-dot" style={{backgroundColor: Colors[i]}}></div>
|
|
|
|
{item}
|
2015-03-16 12:47:39 +00:00
|
|
|
</li>
|
|
|
|
);
|
|
|
|
}.bind(this);
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
var legend = _(paths).pluck(0).filter(function(el){ return el !== null; }).value();
|
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
return (
|
2015-03-16 12:47:39 +00:00
|
|
|
<div ref="container" className="sachart-container">
|
2015-03-14 15:53:57 +00:00
|
|
|
<div className="whatsgoingon">
|
2015-03-16 11:20:32 +00:00
|
|
|
This stacked area chart represents <em>{words.items[this.state.item]}</em> {words.actions[this.state.item]} <em>{who}</em> {words.item[subject]} <WeekIntervalSelector />
|
2015-03-14 15:53:57 +00:00
|
|
|
</div>
|
2015-03-11 12:10:11 +00:00
|
|
|
<div className="filters">
|
2015-03-16 08:36:15 +00:00
|
|
|
<Selector thing="sort"
|
|
|
|
title="Show"
|
|
|
|
items={['commits']}
|
|
|
|
value={'commits'} />
|
2015-03-11 12:10:11 +00:00
|
|
|
<Selector thing="item"
|
2015-03-16 08:36:15 +00:00
|
|
|
title="Grouped by"
|
2015-03-11 12:10:11 +00:00
|
|
|
items={this.props.items}
|
|
|
|
value={this.state.item}
|
|
|
|
onChange={this.handleFilter.bind(this, 'item')} />
|
|
|
|
</div>
|
2015-03-12 13:11:52 +00:00
|
|
|
<svg ref="svg" className="sachart" key="sachart-svg"
|
2015-03-16 16:53:54 +00:00
|
|
|
onMouseOut={this.handleFocusOut}
|
2015-03-11 12:10:11 +00:00
|
|
|
width="100%" height={maxHeight}
|
2015-03-13 12:08:13 +00:00
|
|
|
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ maxHeight}>
|
2015-03-16 12:05:39 +00:00
|
|
|
<g ref="areas">{areas.reverse()}</g>
|
2015-03-16 16:53:54 +00:00
|
|
|
<g ref="dots">{renderedDots}</g>
|
2015-03-11 12:10:11 +00:00
|
|
|
</svg>
|
|
|
|
<ul className="legend">
|
2015-03-16 16:53:54 +00:00
|
|
|
{legend.map(renderLegend)}
|
2015-03-11 12:10:11 +00:00
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var StackedArea = React.createClass({
|
2015-03-13 12:08:13 +00:00
|
|
|
mixins: [ChartAnimationMixin],
|
2015-03-12 13:11:52 +00:00
|
|
|
|
|
|
|
getInitialState: function() {
|
2015-03-13 12:08:13 +00:00
|
|
|
return {};
|
2015-03-12 13:11:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
componentWillReceiveProps: function(newProps) {
|
|
|
|
this.setState({
|
2015-03-13 12:08:13 +00:00
|
|
|
lastd: this.props.d || newProps.d,
|
|
|
|
}, this.animateAll);
|
2015-03-12 13:11:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
animateAll: function() {
|
2015-03-13 12:08:13 +00:00
|
|
|
this.clearAnimations(this.refs.path);
|
2015-03-12 13:11:52 +00:00
|
|
|
this.animate(this.refs.path, 'd', this.state.lastd, this.props.d);
|
|
|
|
},
|
|
|
|
|
2015-03-11 12:10:11 +00:00
|
|
|
render: function() {
|
|
|
|
return (
|
2015-03-12 13:11:52 +00:00
|
|
|
<path ref="path"
|
2015-03-16 12:47:39 +00:00
|
|
|
className={'path path-'+ this.props.i}
|
2015-03-13 12:08:13 +00:00
|
|
|
d={this.state.lastd || this.props.d}
|
2015-03-11 20:18:14 +00:00
|
|
|
fill={this.props.color}
|
2015-03-16 12:47:39 +00:00
|
|
|
onMouseOver={this.props.onMouseOver}
|
2015-03-11 20:18:14 +00:00
|
|
|
shapeRendering="optimizeQuality" />
|
2015-03-11 12:10:11 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2015-03-16 12:05:39 +00:00
|
|
|
|
|
|
|
var Dot = React.createClass({
|
|
|
|
mixins: [ChartAnimationMixin],
|
|
|
|
|
2015-03-16 16:53:54 +00:00
|
|
|
radius: 10,
|
2015-03-16 12:05:39 +00:00
|
|
|
|
|
|
|
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 (
|
2015-03-16 16:53:54 +00:00
|
|
|
<g className={'dot dot-'+ this.props.i} onMouseOver={this.props.onMouseOver}>
|
2015-03-16 12:05:39 +00:00
|
|
|
<circle ref="dot"
|
|
|
|
cx={this.props.x}
|
|
|
|
cy={this.state.lastY || this.props.y}
|
|
|
|
r={this.radius}
|
2015-03-16 16:53:54 +00:00
|
|
|
fill="#fff"
|
|
|
|
stroke="#f0f0f0"
|
|
|
|
strokeWidth="1" />
|
2015-03-16 12:47:39 +00:00
|
|
|
<text ref="value"
|
2015-03-16 16:53:54 +00:00
|
|
|
x={this.props.x}
|
|
|
|
y={this.props.y+4}
|
|
|
|
textAnchor="middle">
|
2015-03-16 12:47:39 +00:00
|
|
|
{this.props.value}
|
|
|
|
</text>
|
2015-03-16 12:05:39 +00:00
|
|
|
</g>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|