Refactor SAC
This commit is contained in:
parent
8eb44d0ae4
commit
d785af38c3
|
@ -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({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue