1
0
Fork 0

Refactor SAC

This commit is contained in:
Gregory Eremin 2015-03-17 14:42:28 +07:00
parent 8eb44d0ae4
commit d785af38c3
1 changed files with 116 additions and 111 deletions

View File

@ -1,11 +1,12 @@
var StackedAreaChart = React.createClass({ var StackedAreaChart = React.createClass({
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin], mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
numElements: 10, canvasHeight: 350,
maxWeeks: 30,
height: 350,
xAxisHeight: 20, xAxisHeight: 20,
maxItems: 10,
maxWeeks: 30,
words: { words: {
items: { items: {
repo: 'repositories', repo: 'repositories',
@ -27,9 +28,9 @@ var StackedAreaChart = React.createClass({
return { return {
item: this.props.items[0], item: this.props.items[0],
rawData: [], rawData: [],
top: [], topItems: [],
weeks: [], weeklyData: [],
max: 1 maxCommitsPerWeek: 1
}; };
}, },
@ -39,19 +40,22 @@ var StackedAreaChart = React.createClass({
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
// If new items are the same as old then don't reset current item
this.setState({ this.setState({
item: (_.isEqual(newProps.items, this.props.items) item: (_.isEqual(newProps.items, this.props.items)
? this.state.item ? this.state.item
: newProps.items[0]), : newProps.items[0]),
state: 'newProps' state: 'loadingData'
}, this.fetchData); }, this.fetchData);
}, },
shouldComponentUpdate: function(newProps, newState) { shouldComponentUpdate: function(newProps, newState) {
// Don't re-render unless canvas width is calculated
if (!newState.canvasWidth) { if (!newState.canvasWidth) {
return false; return false;
} }
if (newState.state !== 'newPoints') { // We're working with animations here so we render only in one particular state
if (newState.state !== 'pleaseRender') {
return false; return false;
} }
return true; return true;
@ -61,7 +65,7 @@ var StackedAreaChart = React.createClass({
if (this.props.items[i] !== this.state.item) { if (this.props.items[i] !== this.state.item) {
this.setState({ this.setState({
item: this.props.items[i], item: this.props.items[i],
state: 'newProps' state: 'loadingData'
}, this.fetchData); }, this.fetchData);
} }
}, },
@ -83,17 +87,18 @@ var StackedAreaChart = React.createClass({
}, },
handleNewData: function() { handleNewData: function() {
// Group commits by items // [week, ...]
var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value(); var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value();
if (weeksList.length < 2) { if (weeksList.length < 2) {
this.setState({ this.setState({
weeks: [], weeks: [],
state: 'newPoints' state: 'pleaseRender'
}); });
return; return;
} }
var counts = _.reduce(this.state.rawData, function(res, el) { // {item: commits, ...}
var commitsByItem = _.reduce(this.state.rawData, function(res, el) {
if (weeksList.indexOf(el.week) === -1) { if (weeksList.indexOf(el.week) === -1) {
return res; return res;
} }
@ -105,108 +110,72 @@ var StackedAreaChart = React.createClass({
return res; return res;
}, {}); }, {});
// Extract top items from // [item, ...]
var top = _(_.pairs(counts)) // Take [item, count] pairs from counts object var topItems = _(_.pairs(commitsByItem)) // Take [item, count] pairs from counts object
.sortBy(1).reverse() // sort them by count (descending) .sortBy(1).reverse() // sort them by count (descending)
.take(this.numElements) // take first N pairs .take(this.maxItems) // take first N pairs
.pluck(0) // keep only items, omit the counts .pluck(0) // keep only items, omit the counts
.value(); .value();
for (var i = top.length; i < this.numElements; i++) { for (var i = topItems.length; i < this.maxItems; i++) {
top[i] = null; topItems[i] = null;
}; };
var weeks = _.reduce(this.state.rawData, function(res, el) { // {week: {item: commits, ...}, ...}
var weeklyData = _.reduce(this.state.rawData, function(res, el) {
if (weeksList.indexOf(el.week) === -1) { if (weeksList.indexOf(el.week) === -1) {
return res; return res;
} }
if (res[el.week] === undefined) { if (res[el.week] === undefined) {
res[el.week] = {}; res[el.week] = {};
} }
if (top.indexOf(el.item) > -1) { if (topItems.indexOf(el.item) > -1) {
res[el.week][el.item] = el.commits; res[el.week][el.item] = el.commits;
} }
return res; return res;
}, {}); }, {});
var max = _.max(_.map(weeksList, function(week){ return _.sum(_.values(weeks[week])); })); var maxCommitsPerWeek = _.max(_.map(weeksList, function(week) {
return _.sum(_.values(weeklyData[week]));
}));
this.setState({ this.setState({
top: top, topItems: topItems,
weeks: weeks, weeklyData: weeklyData,
max: max, maxCommitsPerWeek: maxCommitsPerWeek,
state: 'newPoints' state: 'pleaseRender'
}); });
}, },
buildPathD: function(points) { buildPathD: function(dots) {
var maxWidth = this.state.canvasWidth, var maxWidth = this.state.canvasWidth,
maxHeight = this.height; maxHeight = this.canvasHeight;
var dots = this.buildDots(points); var dots = this.extendDotsWithCoordinates(dots);
var first = dots.shift(); var first = dots.shift(); // Don't draw a line to the first dot, it should be a move
var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; }); var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; });
d.unshift('M'+ first.x +','+ first.y); d.unshift('M'+ first.x +','+ first.y); // Prepend first move
d.push('L'+ maxWidth +','+ maxHeight); d.push('L'+ maxWidth +','+ maxHeight); // Draw a line to the bottom right corner
d.push('L0,'+ maxHeight +' Z'); d.push('L0,'+ maxHeight +' Z'); // And then to a bottom left corner
return d.join(' '); return d.join(' ');
}, },
buildDots: function(points) { extendDotsWithCoordinates: function(dots) {
var maxWidth = this.state.canvasWidth, var maxWidth = this.state.canvasWidth,
maxHeight = this.height, maxHeight = this.canvasHeight,
maxValue = this.state.max, maxValue = this.state.maxCommitsPerWeek,
len = points.length; len = dots.length;
return _.map(points, function(point, i) { return _.map(dots, function(dot, i) {
point.x = i/(len-1)*maxWidth; dot.x = i/(len-1)*maxWidth;
point.y = maxHeight - point.point; dot.y = maxHeight - dot.norm*maxHeight*0.96;
return point; return dot;
}); });
}, },
render: function() { render: function() {
var maxWidth = this.state.canvasWidth, var renderArea = function(pair, i) {
maxHeight = this.height, var item = pair[0], path = pair[1];
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 ( return (
<StackedArea key={'area-'+ i} <StackedArea key={'area-'+ i}
item={item} i={i} item={item} i={i}
@ -214,17 +183,7 @@ var StackedAreaChart = React.createClass({
color={Colors[i]} color={Colors[i]}
onMouseOver={this.handleFocusIn.bind(this, i)} /> onMouseOver={this.handleFocusIn.bind(this, i)} />
); );
}.bind(this)); }.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) { var renderDot = function(item, i, dot, j) {
if (dot.val === 0) { if (dot.val === 0) {
@ -232,7 +191,7 @@ var StackedAreaChart = React.createClass({
} }
var maxWidth = this.state.canvasWidth, var maxWidth = this.state.canvasWidth,
maxHeight = this.height, maxHeight = this.canvasHeight,
radius = 10, radius = 10,
x = dot.x, x = dot.x,
y = dot.y; y = dot.y;
@ -258,12 +217,6 @@ var StackedAreaChart = React.createClass({
); );
}.bind(this); }.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){ var renderLegend = function(item, i){
return ( return (
<li key={'legend-'+ item} <li key={'legend-'+ item}
@ -278,7 +231,60 @@ var StackedAreaChart = React.createClass({
); );
}.bind(this); }.bind(this);
var legend = _(paths).pluck(0).filter(function(el){ return el !== null; }).value(); 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,
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];
return ( return (
<div ref="container" className="sac"> <div ref="container" className="sac">
@ -298,15 +304,15 @@ var StackedAreaChart = React.createClass({
</div> </div>
<svg ref="svg" className="sachart" key="sachart-svg" <svg ref="svg" className="sachart" key="sachart-svg"
width="100%" width="100%"
height={this.height + this.xAxisHeight} height={this.canvasHeight + this.xAxisHeight}
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ (this.height + this.xAxisHeight)} viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ (this.canvasHeight + this.xAxisHeight)}
onMouseOut={this.handleFocusOut} onMouseOut={this.handleFocusOut}
> >
<g ref="areas">{areas.reverse()}</g> <g ref="areas">{dotsByItem.map(renderArea).reverse()}</g>
<g ref="dots">{renderedDots}</g> <g ref="dots">{renderedDots}</g>
<Axis <Axis
weeks={_.pluck(points, 0)} weeks={_.pluck(dotsByWeek, 0)}
y={this.height + 3} y={this.canvasHeight + 3}
width={this.state.canvasWidth} /> width={this.state.canvasWidth} />
</svg> </svg>
<ul className="legend"> <ul className="legend">
@ -393,15 +399,15 @@ var Axis = React.createClass({
var len = this.props.weeks.length, var len = this.props.weeks.length,
x = i/(len - 1)*this.props.width, x = i/(len - 1)*this.props.width,
showLabel, showLabel,
ta = (i === 0 ta = (i === 0 // Text anchor for the leftmost label
? 'start' ? 'start'
: (i === len - 1 : (i === len - 1 // Text anchor for the rightmost label
? 'end' ? 'end'
: 'middle')); : 'middle')); // Text anchor for other labels
// Thin out labels // Thin out labels
if (len > 20) { if (len > 20) {
showLabel = (i % 2 === 0); showLabel = (i % 3 === 0);
} else if (len > 10) { } else if (len > 10) {
showLabel = (i % 2 === 0); showLabel = (i % 2 === 0);
} else { } else {
@ -415,13 +421,13 @@ var Axis = React.createClass({
y1={this.props.y} y1={this.props.y}
x2={x} x2={x}
y2={this.props.y + 4} /> y2={this.props.y + 4} />
{showLabel ? <text className="axis-mark" {!showLabel ? null : <text className="axis-mark"
x={x} x={x}
y={this.props.y + 15} y={this.props.y + 15}
textAnchor={ta} textAnchor={ta}
> >
{formatDate(week)} {formatDate(week)}
</text> : null} </text>}
</g> </g>
); );
}.bind(this); }.bind(this);
@ -443,4 +449,3 @@ var Axis = React.createClass({
) )
} }
}); });