Split JS code by classes into different files
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
var Area = 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 (
|
||||
<path ref="path"
|
||||
className={'path path-'+ this.props.i}
|
||||
d={this.state.lastd || this.props.d}
|
||||
fill={this.props.color}
|
||||
onMouseOver={this.props.onMouseOver}
|
||||
shapeRendering="optimizeQuality" />
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
var Axis = React.createClass({
|
||||
topMargin: 2,
|
||||
markHeight: 5,
|
||||
|
||||
render: function() {
|
||||
if (this.props.weeks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var renderMark = function(week, i) {
|
||||
var len = this.props.weeks.length,
|
||||
x = i/(len - 1)*this.props.width,
|
||||
showLabel,
|
||||
ta = (i === 0 // Text anchor for the leftmost label
|
||||
? 'start'
|
||||
: (i === len - 1 // Text anchor for the rightmost label
|
||||
? 'end'
|
||||
: 'middle')); // Text anchor for other labels
|
||||
|
||||
// Thin out labels
|
||||
if (len > 20) {
|
||||
showLabel = (i % 3 === 0);
|
||||
} else if (len > 10) {
|
||||
showLabel = (i % 2 === 0);
|
||||
} else {
|
||||
showLabel = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<g key={'mark-'+ i}>
|
||||
<line className="axis"
|
||||
x1={x}
|
||||
y1={this.props.y + this.topMargin}
|
||||
x2={x}
|
||||
y2={this.props.y + this.topMargin + this.markHeight} />
|
||||
{!showLabel ? null : <text className="axis-mark"
|
||||
x={x}
|
||||
y={this.props.y + this.topMargin + 14}
|
||||
textAnchor={ta}
|
||||
>
|
||||
{formatDate(week)}
|
||||
</text>}
|
||||
</g>
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
return (
|
||||
<g ref="axis">
|
||||
<rect // This rect hides area bouncing glitches
|
||||
x="0"
|
||||
y={this.props.y}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
fill="#fff" />
|
||||
<line className="axis"
|
||||
x1="0"
|
||||
y1={this.props.y + this.topMargin}
|
||||
x2={this.props.width}
|
||||
y2={this.props.y + this.topMargin} />
|
||||
{this.props.weeks.map(renderMark)}
|
||||
<line className="axis"
|
||||
x1={this.props.width - 1}
|
||||
y1={this.props.y + this.topMargin}
|
||||
x2={this.props.width - 1}
|
||||
y2={this.props.y + this.topMargin + this.markHeight} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
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 (
|
||||
<g className={'dot dot-'+ this.props.i} onMouseOver={this.props.onMouseOver}>
|
||||
<circle ref="dot"
|
||||
cx={this.props.x}
|
||||
cy={this.state.lastY || this.props.y}
|
||||
r={this.radius} />
|
||||
<text ref="value"
|
||||
x={this.props.x}
|
||||
y={this.props.y+4}>
|
||||
{this.props.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
var StackedAreaChart = React.createClass({
|
||||
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
|
||||
|
||||
canvasHeight: 350,
|
||||
xAxisHeight: 20,
|
||||
|
||||
maxItems: 10,
|
||||
maxWeeks: 30,
|
||||
|
||||
words: {
|
||||
actions: { // Item
|
||||
repo: "made to",
|
||||
team: "made by the most active",
|
||||
user: "made by the most active"
|
||||
},
|
||||
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"
|
||||
},
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
item: this.props.items[0],
|
||||
rawData: [],
|
||||
topItems: [],
|
||||
weeklyData: [],
|
||||
maxCommitsPerWeek: 1
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.calculateViewBoxWidth();
|
||||
window.addEventListener('resize', this.calculateViewBoxWidth);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// If new items are the same as old then don't reset current item
|
||||
this.setState({
|
||||
item: (_.isEqual(newProps.items, this.props.items)
|
||||
? this.state.item
|
||||
: newProps.items[0]),
|
||||
state: 'loadingData'
|
||||
}, this.fetchData);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(newProps, newState) {
|
||||
// Don't re-render unless canvas width is calculated
|
||||
if (!newState.canvasWidth) {
|
||||
return false;
|
||||
}
|
||||
// We're working with animations here so we render only in one particular state
|
||||
if (newState.state !== 'pleaseRender') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
handleFilter: function(thing, i) {
|
||||
if (this.props.items[i] !== this.state.item) {
|
||||
this.setState({
|
||||
item: this.props.items[i],
|
||||
state: 'loadingData'
|
||||
}, this.fetchData);
|
||||
}
|
||||
},
|
||||
|
||||
handleClick: function(item) {
|
||||
this.handleFocusOut();
|
||||
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 = 'sac focused item-'+ i;
|
||||
},
|
||||
|
||||
handleFocusOut: function() {
|
||||
var node = this.refs.container.getDOMNode();
|
||||
node.className = 'sac';
|
||||
},
|
||||
|
||||
handleNewData: function() {
|
||||
// [week, ...]
|
||||
var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value();
|
||||
if (weeksList.length < 2) {
|
||||
this.setState({
|
||||
topItems: [],
|
||||
weeklyData: {},
|
||||
maxCommitsPerWeek: 0,
|
||||
state: 'pleaseRender'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// {item: commits, ...}
|
||||
var commitsByItem = _.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;
|
||||
}, {});
|
||||
|
||||
// [item, ...]
|
||||
var topItems = _(_.pairs(commitsByItem)) // Take [item, count] pairs from counts object
|
||||
.sortBy(1).reverse() // sort them by count (descending)
|
||||
.take(this.maxItems) // take first N pairs
|
||||
.pluck(0) // keep only items, omit the counts
|
||||
.value();
|
||||
for (var i = topItems.length; i < this.maxItems; i++) {
|
||||
topItems[i] = null;
|
||||
};
|
||||
|
||||
// {week: {item: commits, ...}, ...}
|
||||
var weeklyData = _.reduce(this.state.rawData, function(res, el) {
|
||||
if (weeksList.indexOf(el.week) === -1) {
|
||||
return res;
|
||||
}
|
||||
if (res[el.week] === undefined) {
|
||||
res[el.week] = {};
|
||||
}
|
||||
if (topItems.indexOf(el.item) > -1) {
|
||||
res[el.week][el.item] = el.commits;
|
||||
}
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
var maxCommitsPerWeek = _.max(_.map(weeksList, function(week) {
|
||||
return _.sum(_.values(weeklyData[week]));
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
topItems: topItems,
|
||||
weeklyData: weeklyData,
|
||||
maxCommitsPerWeek: maxCommitsPerWeek,
|
||||
state: 'pleaseRender'
|
||||
});
|
||||
},
|
||||
|
||||
buildPathD: function(dots) {
|
||||
var maxWidth = this.state.canvasWidth,
|
||||
maxHeight = this.canvasHeight;
|
||||
|
||||
var dots = this.extendDotsWithCoordinates(dots);
|
||||
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; });
|
||||
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
|
||||
|
||||
return d.join(' ');
|
||||
},
|
||||
|
||||
extendDotsWithCoordinates: function(dots) {
|
||||
var maxWidth = this.state.canvasWidth,
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var renderArea = function(pair, i) {
|
||||
var item = pair[0], path = pair[1];
|
||||
// NOTE: Rounded bottom corners is a side-effect
|
||||
return (
|
||||
<Area key={'area-'+ i}
|
||||
item={item} i={i}
|
||||
d={roundPathCorners(this.buildPathD(path), 3)}
|
||||
color={Colors[i]}
|
||||
onMouseOver={this.handleFocusIn.bind(this, i)} />
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
var renderDot = function(item, i, dot, j) {
|
||||
if (dot.val === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var maxWidth = this.state.canvasWidth,
|
||||
maxHeight = this.canvasHeight,
|
||||
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 (
|
||||
<Dot key={'dot-'+ i +'-'+ j}
|
||||
item={item} i={i}
|
||||
value={dot.val}
|
||||
x={x}
|
||||
y={y}
|
||||
onMouseOver={this.handleFocusIn.bind(this, i)} />
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
var renderLegend = function(item, i){
|
||||
return (
|
||||
<li key={'legend-'+ item}
|
||||
className={'label label-'+ i}
|
||||
onMouseOver={this.handleFocusIn.bind(this, i)}
|
||||
onMouseOut={this.handleFocusOut.bind(this, i)}
|
||||
onClick={this.handleClick.bind(this, item)}
|
||||
>
|
||||
<div className="color-dot" style={{backgroundColor: Colors[i]}}></div>
|
||||
{item}
|
||||
</li>
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
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,
|
||||
target = (this.getParams().repo ? 'repo'
|
||||
: this.getParams().team ? 'team'
|
||||
: this.getParams().user ? 'user'
|
||||
: 'org');
|
||||
subject = this.getParams()[target];
|
||||
|
||||
return (
|
||||
<div ref="container" className="sac">
|
||||
<div className="whatsgoingon">
|
||||
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>
|
||||
<div className="filters">
|
||||
<Selector thing="sort"
|
||||
title="Show"
|
||||
items={['commits']}
|
||||
value={'commits'} />
|
||||
<Selector thing="item"
|
||||
title="Grouped by"
|
||||
items={this.props.items}
|
||||
value={this.state.item}
|
||||
onChange={this.handleFilter.bind(this, 'item')} />
|
||||
</div>
|
||||
<svg ref="svg" className="sachart" key="sachart-svg"
|
||||
width="100%"
|
||||
height={this.canvasHeight + this.xAxisHeight}
|
||||
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ (this.canvasHeight + this.xAxisHeight)}
|
||||
onMouseOut={this.handleFocusOut}
|
||||
>
|
||||
<g ref="areas">{dotsByItem.map(renderArea).reverse()}</g>
|
||||
<g ref="dots">{renderedDots}</g>
|
||||
<Axis
|
||||
weeks={_.pluck(dotsByWeek, 0)}
|
||||
y={this.canvasHeight}
|
||||
width={this.state.canvasWidth}
|
||||
height={this.xAxisHeight} />
|
||||
</svg>
|
||||
<ul className="legend">
|
||||
{legend.map(renderLegend)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user