1
0
Fork 0
empact/app/jsx/charts/sac/stacked_area_chart.jsx

337 lines
11 KiB
React
Raw Normal View History

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
2015-03-17 07:42:28 +00:00
canvasHeight: 350,
2015-03-16 18:02:53 +00:00
xAxisHeight: 20,
2015-03-17 07:42:28 +00:00
maxItems: 10,
maxWeeks: 30,
2015-03-16 12:05:39 +00:00
words: {
2015-03-17 18:24:02 +00:00
actions: { // Item
repo: "made to",
team: "made by the most active",
user: "made by the most active"
2015-03-16 12:05:39 +00:00
},
2015-03-17 18:24:02 +00:00
items: { // Item
repo: "repositories",
team: "teams",
user: "users"
},
whatHappened: { // Item-Target
"user-repo": "working on",
"team-repo": "working on",
"team-org": "to repositories of",
"user-org": "to repositories of",
"repo-org": "that were most actively modified by the members of",
"user-team": "working on repositories of the",
"repo-team": "that were most actively modified by the members of the",
"repo-user": "that were most actively modified by"
},
targetSuffix: { // Subject of current context
repo: "repository",
team: "team"
2015-03-16 12:05:39 +00:00
},
},
2015-03-11 12:10:11 +00:00
getInitialState: function() {
return {
item: this.props.items[0],
rawData: [],
2015-03-17 07:42:28 +00:00
topItems: [],
weeklyData: [],
maxCommitsPerWeek: 1
2015-03-11 12:10:11 +00:00
};
},
componentDidMount: function() {
this.calculateViewBoxWidth();
window.addEventListener('resize', this.calculateViewBoxWidth);
},
componentWillReceiveProps: function(newProps) {
2015-03-17 07:42:28 +00:00
// If new items are the same as old then don't reset current item
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-17 07:42:28 +00:00
state: 'loadingData'
}, this.fetchData);
},
2015-03-12 13:11:52 +00:00
shouldComponentUpdate: function(newProps, newState) {
2015-03-17 07:42:28 +00:00
// Don't re-render unless canvas width is calculated
2015-03-13 12:08:13 +00:00
if (!newState.canvasWidth) {
2015-03-12 13:11:52 +00:00
return false;
}
2015-03-17 07:42:28 +00:00
// We're working with animations here so we render only in one particular state
if (newState.state !== 'pleaseRender') {
2015-03-12 13:11:52 +00:00
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],
2015-03-17 07:42:28 +00:00
state: 'loadingData'
2015-03-11 12:10:11 +00:00
}, this.fetchData);
}
},
2015-03-16 17:17:04 +00:00
handleClick: function(item) {
2015-03-18 09:37:55 +00:00
this.handleFocusOut();
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();
2015-03-17 06:48:12 +00:00
node.className = 'sac focused item-'+ i;
2015-03-16 12:47:39 +00:00
},
2015-03-16 16:53:54 +00:00
handleFocusOut: function() {
2015-03-16 12:47:39 +00:00
var node = this.refs.container.getDOMNode();
2015-03-17 06:48:12 +00:00
node.className = 'sac';
2015-03-16 12:47:39 +00:00
},
2015-03-13 12:08:13 +00:00
handleNewData: function() {
2015-03-17 07:42:28 +00:00
// [week, ...]
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({
2015-03-18 09:35:54 +00:00
topItems: [],
weeklyData: {},
maxCommitsPerWeek: 0,
2015-03-17 07:42:28 +00:00
state: 'pleaseRender'
2015-03-16 17:17:04 +00:00
});
return;
}
2015-03-12 13:45:29 +00:00
2015-03-17 07:42:28 +00:00
// {item: commits, ...}
var commitsByItem = _.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;
}, {});
2015-03-17 07:42:28 +00:00
// [item, ...]
var topItems = _(_.pairs(commitsByItem)) // Take [item, count] pairs from counts object
2015-03-11 12:10:11 +00:00
.sortBy(1).reverse() // sort them by count (descending)
2015-03-17 07:42:28 +00:00
.take(this.maxItems) // take first N pairs
2015-03-11 12:10:11 +00:00
.pluck(0) // keep only items, omit the counts
.value();
2015-03-17 07:42:28 +00:00
for (var i = topItems.length; i < this.maxItems; i++) {
topItems[i] = null;
2015-03-12 13:45:29 +00:00
};
2015-03-11 12:10:11 +00:00
2015-03-17 07:42:28 +00:00
// {week: {item: commits, ...}, ...}
var weeklyData = _.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-17 07:42:28 +00:00
if (topItems.indexOf(el.item) > -1) {
res[el.week][el.item] = el.commits;
}
2015-03-11 12:10:11 +00:00
return res;
}, {});
2015-03-17 07:42:28 +00:00
var maxCommitsPerWeek = _.max(_.map(weeksList, function(week) {
return _.sum(_.values(weeklyData[week]));
}));
2015-03-12 13:45:29 +00:00
2015-03-11 12:10:11 +00:00
this.setState({
2015-03-17 07:42:28 +00:00
topItems: topItems,
weeklyData: weeklyData,
maxCommitsPerWeek: maxCommitsPerWeek,
state: 'pleaseRender'
2015-03-11 12:10:11 +00:00
});
},
2015-03-17 07:42:28 +00:00
buildPathD: function(dots) {
2015-03-11 12:10:11 +00:00
var maxWidth = this.state.canvasWidth,
2015-03-17 07:42:28 +00:00
maxHeight = this.canvasHeight;
2015-03-13 12:08:13 +00:00
2015-03-17 07:42:28 +00:00
var dots = this.extendDotsWithCoordinates(dots);
var first = dots.shift(); // Don't draw a line to the first dot, it should be a move
2015-03-16 16:53:54 +00:00
var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; });
2015-03-17 07:42:28 +00:00
d.unshift('M'+ first.x +','+ first.y); // Prepend first move
d.push('L'+ maxWidth +','+ maxHeight); // Draw a line to the bottom right corner
d.push('L0,'+ maxHeight +' Z'); // And then to a bottom left corner
2015-03-11 12:10:11 +00:00
return d.join(' ');
},
2015-03-17 07:42:28 +00:00
extendDotsWithCoordinates: function(dots) {
2015-03-16 12:05:39 +00:00
var maxWidth = this.state.canvasWidth,
2015-03-17 07:42:28 +00:00
maxHeight = this.canvasHeight,
maxValue = this.state.maxCommitsPerWeek,
len = dots.length;
return _.map(dots, function(dot, i) {
dot.x = i/(len-1)*maxWidth;
dot.y = maxHeight - dot.norm*maxHeight*0.96;
return dot;
2015-03-16 12:05:39 +00:00
});
},
2015-03-11 12:10:11 +00:00
render: function() {
2015-03-17 07:42:28 +00:00
var renderArea = function(pair, i) {
var item = pair[0], path = pair[1];
2015-03-17 19:13:16 +00:00
// NOTE: Rounded bottom corners is a side-effect
2015-03-11 12:10:11 +00:00
return (
<Area key={'area-'+ i}
2015-03-16 12:47:39 +00:00
item={item} i={i}
d={roundPathCorners(this.buildPathD(path), 3)}
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
);
2015-03-17 07:42:28 +00:00
}.bind(this);
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,
2015-03-17 07:42:28 +00:00
maxHeight = this.canvasHeight,
2015-03-16 17:17:04 +00:00
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 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-17 07:42:28 +00:00
var maxWidth = this.state.canvasWidth,
maxHeight = this.canvasHeight,
top = this.state.topItems,
max = this.state.maxCommitsPerWeek;
// [week, [dot, ...]]
var dotsByWeek = _(this.state.weeklyData)
.map(function(items, week) {
var values = _.map(top, function(item) {
return items[item] || 0;
});
var sum = 0;
var dots = _.map(values, function(val) {
sum += val/max;
return {
val: val,
norm: sum
};
});
return [parseInt(week, 10), dots];
})
.sort(0)
.reverse()
.take(this.maxWeeks)
.reverse()
.value();
// [item, [dot, ...]]
var dotsByItem = _.map(top, function(item, i) {
var dots = _.map(dotsByWeek, function(pair) {
var dots = pair[1];
return dots[i];
});
return[item, dots];
});
var renderedDots = _.map(dotsByItem, function(pair, i) {
var item = pair[0], path = pair[1];
var dots = this.extendDotsWithCoordinates(path);
return dots.map(renderDot.bind(this, item, i));
}.bind(this));
var legend = _(dotsByItem).pluck(0).filter(function(el){ return el !== null; }).value();
// Text generation stuff
var words = this.words,
2015-03-17 18:24:02 +00:00
target = (this.getParams().repo ? 'repo'
: this.getParams().team ? 'team'
: this.getParams().user ? 'user'
: 'org');
subject = this.getParams()[target];
2015-03-16 16:53:54 +00:00
2015-03-11 12:10:11 +00:00
return (
2015-03-17 06:48:12 +00:00
<div ref="container" className="sac">
<div className="whatsgoingon">
2015-03-17 18:24:02 +00:00
This stacked area chart shows <em>the number of commits</em> {words.actions[this.state.item]} <em>{words.items[this.state.item]}</em> {words.whatHappened[this.state.item +'-'+ target]} <em>{subject}</em> {words.targetSuffix[target]} <WeekIntervalSelector />
</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 18:02:53 +00:00
width="100%"
2015-03-17 07:42:28 +00:00
height={this.canvasHeight + this.xAxisHeight}
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ (this.canvasHeight + this.xAxisHeight)}
2015-03-16 16:53:54 +00:00
onMouseOut={this.handleFocusOut}
2015-03-16 18:02:53 +00:00
>
2015-03-17 07:42:28 +00:00
<g ref="areas">{dotsByItem.map(renderArea).reverse()}</g>
2015-03-16 16:53:54 +00:00
<g ref="dots">{renderedDots}</g>
2015-03-16 18:02:53 +00:00
<Axis
2015-03-17 07:42:28 +00:00
weeks={_.pluck(dotsByWeek, 0)}
2015-03-17 19:13:16 +00:00
y={this.canvasHeight}
width={this.state.canvasWidth}
height={this.xAxisHeight} />
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>
);
}
});