Both charts working great!

This commit is contained in:
2015-03-13 19:08:13 +07:00
parent dbd943333b
commit 997d77ed00
9 changed files with 421 additions and 426 deletions
+28
View File
@@ -0,0 +1,28 @@
var ChartAnimationMixin = {
animDuration: 350,
animate: function(ref, attr, from, to) {
var node = ref.getDOMNode(),
anim = anim = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
anim.setAttributeNS(null, 'attributeType', 'XML');
anim.setAttributeNS(null, 'attributeName', attr);
anim.setAttributeNS(null, 'values', from +';'+ to);
anim.setAttributeNS(null, 'dur', this.animDuration +'ms');
anim.setAttributeNS(null, 'calcMode', 'spline');
anim.setAttributeNS(null, 'keySplines', this.easing);
anim.setAttributeNS(null, 'repeatCount', '1');
node.appendChild(anim);
anim.beginElement();
setTimeout(function() {
node.setAttributeNS(null, attr, to);
}, this.animDuration);
},
clearAnimations: function(ref) {
var node = ref.getDOMNode();
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
};
+268
View File
@@ -0,0 +1,268 @@
var BarChart = React.createClass({
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
sorts: ['commits', 'delta'],
numElements: 15,
barHeight: 30,
barMargin: 5,
getInitialState: function() {
return {
item: this.props.items[0],
sort: 'commits',
rawData: [],
points: [],
min: 0,
max: 1
};
},
componentDidMount: function() {
this.calculateViewBoxWidth();
window.addEventListener('resize', this.calculateViewBoxWidth);
this.componentWillReceiveProps(this.props);
},
componentWillReceiveProps: function(newProps) {
this.setState({
item: newProps.items[0],
sort: 'commits',
state: 'newProps'
}, this.fetchData);
},
shouldComponentUpdate: function(newProps, newState) {
if (!newState.canvasWidth) {
return false;
}
if (newState.state !== 'newPoints') {
return false;
}
return true;
},
handleFilter: function(thing, i) {
if (thing === 'item' && this.props.items[i] !== this.state.item) {
this.setState({
item: this.props.items[i],
state: 'newProps'
}, this.fetchData);
} else if (thing === 'sort' && this.sorts[i] !== this.state.sort) {
this.setState({
sort: this.sorts[i],
state: 'newProps'
}, this.handleNewData);
}
},
handleClick: function(point) {
var params = {org: this.getParams().org};
params[this.state.item] = point.item;
this.transitionTo(this.state.item, params);
},
handleNewData: function() {
var min = 0, max = 1;
var points = _.chain(this.state.rawData)
.sort(function(a, b) {
return Math.abs(b[this.state.sort]) - Math.abs(a[this.state.sort]);
}.bind(this))
.take(this.numElements)
.value();
for (var i = points.length; i < this.numElements; i++) {
var point = {};
point[this.state.sort] = 0;
points.push(point);
}
// console.log("Setting points!");
this.setState({
points: points,
min: _.min(points, this.state.sort)[this.state.sort],
max: _.max(points, this.state.sort)[this.state.sort],
state: 'newPoints'
});
},
height: function() {
if (this.state.points.length === 0) {
return 0;
} else {
return this.y(this.state.points.length) - this.barMargin;
}
},
y: function(i) {
return i*(this.barHeight + this.barMargin);
},
render: function() {
// console.log("Render barchart!", this.state);
return (
<div className="barchart-container">
<div className="filters">
<Selector thing="item"
items={this.props.items}
value={this.state.item}
onChange={this.handleFilter.bind(this, 'item')} />
<Selector thing="sort"
items={this.sorts}
value={this.state.sort}
onChange={this.handleFilter.bind(this, 'sort')} />
</div>
<svg ref="svg" className="barchart" key="barchart-svg"
width="100%" height={this.height()}
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ this.height()}>
{this.state.points.map(this.renderBar)}
</svg>
</div>
);
},
renderBar: function(point, i) {
var maxWidth = this.state.canvasWidth,
val = point[this.state.sort],
min = this.state.min,
max = this.state.max,
max2 = (min < 0 ? max - min : max),
width = Math.floor(Math.abs(val)/max2*maxWidth),
height = this.barHeight,
offset = (min < 0 ? -min : 0)/max2*maxWidth,
x = (min >= 0 ? 0 : offset - (val >= 0 ? 0 : width)),
y = this.y(i);
return (
<Bar key={'bar-'+ i}
item={point.item}
value={val}
color={Colors2[i]}
x={x}
y={y}
offset={offset}
width={width}
height={height}
max={maxWidth}
onClick={this.handleClick.bind(this, point)} />
);
}
});
var Bar = React.createClass({
mixins: [ReactRouter.Navigation, ChartAnimationMixin],
// easing: '0.075 0.82 0.165 1', // easeOutCirc
easing: '0.175 0.885 0.32 1.275', // easeOutBack
height: 30,
labelPaddingH: 5, // Label horizontal padding
labelPaddingV: 2, // Label vertical padding
labelMarginV: 5, // Same as padding
labelHeight: 16, // Text size
labelOuterHeight: 20, // labelHeight + 2*labelPaddingV,
getInitialState: function() {
return {
labelX: 0,
lastLabelX: 2*this.labelPaddingH
};
},
componentDidMount: function() {
this.calculateLabelPosition();
},
componentWillReceiveProps: function(newProps) {
if (_.isEqual(this.props, newProps)) {
return;
}
// console.log("New bar props!", newProps.item, newProps.x, newProps.width);
this.setState({
lastBarX: (this.props.x !== undefined ? this.props.x : newProps.x),
lastBarWidth: (this.props.width !== undefined ? this.props.width : newProps.width),
lastLabelX: this.state.labelX
}, this.calculateLabelPosition);
},
calculateLabelPosition: function() {
var val = this.props.value,
offset = this.props.offset,
width = this.props.width,
label = this.props.item + ': ' + val,
labelWidth = textWidth(label),
labelOuterWidth = labelWidth + 2*this.labelPaddingH,
labelOffsetWidth = labelOuterWidth + 2*this.labelPaddingH,
labelMarginV = (this.props.height - this.labelOuterHeight)/2,
labelX,
labelY = this.props.y + this.labelOuterHeight + 1, // 1 is magic
barX = this.props.x,
barX2 = barX + width;
if (offset === 0) {
labelX = 2*this.labelPaddingH;
} else {
if (val < 0) {
if (offset >= labelOffsetWidth) {
labelX = offset - labelOffsetWidth + 2*this.labelPaddingH;
} else {
labelX = offset + 2*this.labelPaddingH;
}
} else {
if (offset + labelOffsetWidth <= this.props.max) {
labelX = offset + 2*this.labelPaddingH;
} else {
labelX = offset - labelOffsetWidth + 2*this.labelPaddingH;
}
}
}
this.setState({
labelX: labelX
}, this.animateAll);
},
animateAll: function() {
// console.log("animate bar!", this.state, this.props);
this.clearAnimations(this.refs.bar);
this.clearAnimations(this.refs.underlay);
this.animate(this.refs.bar, 'width', this.state.lastBarWidth, this.props.width);
this.animate(this.refs.bar, 'x', this.state.lastBarX, this.props.x);
var ph = this.labelPaddingH;
this.animate(this.refs.underlay, 'x', this.state.lastLabelX - ph, this.state.labelX - ph);
// this.animate(this.refs.label, 'x', this.state.lastLabelX, this.state.labelX);
},
render: function() {
var label = this.props.item ? (this.props.item + ': ' + this.props.value) : '',
labelWidth = textWidth(label),
labelOuterWidth = labelWidth + 2*this.labelPaddingH;
// var width = this.state.lastBarWidth === 0 ? this.props.width : this.state.lastBarWidth;
return (
<g onClick={this.props.onClick}>
<rect ref="bar"
className="bar"
fill={this.props.color}
width={this.props.width}
height={this.props.height}
x={this.props.x}
y={this.props.y}
rx="2"
ry="2" />
<rect ref="underlay"
className="label_underlay"
width={labelOuterWidth}
height={this.labelOuterHeight}
x={this.state.labelX - this.labelPaddingH}
y={this.props.y + this.labelMarginV}
rx="3"
ry="3" />
<text ref="label"
className="label"
x={this.state.labelX}
y={this.props.y + this.labelOuterHeight + 1}>
{label}</text>
</g>
);
}
});
+31
View File
@@ -0,0 +1,31 @@
var SVGChartMixin = {
calculateViewBoxWidth: function() {
this.setState({
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
});
}
};
var fontFamily = 'Helvetica Neue, Helvetica, sans-serif',
fontSize = 16;
function textWidth(str) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', "svg");
text = document.createElementNS('http://www.w3.org/2000/svg', "text");
svg.width = 500;
svg.height = 500;
svg.style.position = 'absolute';
svg.style.left = '-1000px';
text.appendChild(document.createTextNode(str))
text.style.fontFamily = fontFamily;
text.style.fontSize = fontSize +'px';
svg.appendChild(text);
document.body.appendChild(svg);
var box = text.getBBox();
document.body.removeChild(svg);
return box.width;
}
+44
View File
@@ -0,0 +1,44 @@
var ChartDataMixin = {
apiParams: function() {
var params = _.clone(this.props.params);
params['item'] = this.state.item;
return params;
},
apiParamsHash: function() {
var params = this.apiParams();
var pairs = _.chain(params)
.keys()
.sort()
.map(function(param) {
return [param, params[param]];
})
.value();
return JSON.stringify(pairs);
},
fetchData: function() {
var paramsHash = this.apiParamsHash();
if (!this.state.item) {
return;
}
if (this.state.currentApi === this.props.api && this.state.currentParams === paramsHash) {
return;
}
// console.log('-----> fetching', this.props.api, this.state.item);
this.setState({
currentApi: this.props.api,
currentParams: paramsHash,
state: 'loadingData'
}, function() {
$.get(this.props.api, this.apiParams(), function(res){
this.setState({
rawData: res,
state: 'newData'
}, this.handleNewData);
}.bind(this));
}.bind(this));
}
};
+28
View File
@@ -0,0 +1,28 @@
var Selector = React.createClass({
names: {
"repo": "Repositories",
"team": "Teams",
"user": "Users",
"commits": "Commits",
"delta": "Delta"
},
renderItem: function(item, i) {
var itemClass = (item === this.props.value ? 'active' : ''),
clickEvent = null;
if (this.props.onChange) {
clickEvent = this.props.onChange.bind(this, i);
}
return (
<li key={item} onClick={clickEvent} className={itemClass}>{this.names[item]}</li>
);
},
render: function() {
return (
<ul className={this.props.thing}>
{this.props.items.map(this.renderItem)}
</ul>
);
}
});
@@ -0,0 +1,233 @@
var StackedAreaChart = React.createClass({
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
numElements: 10,
maxWeeks: 20,
height: 250,
getInitialState: function() {
return {
item: this.props.items[0],
rawData: [],
top: [],
weeks: [],
max: 1
};
},
componentDidMount: function() {
this.calculateViewBoxWidth();
window.addEventListener('resize', this.calculateViewBoxWidth);
},
componentWillReceiveProps: function(newProps) {
this.setState({
item: newProps.items[0],
sort: 'commits',
state: 'newProps'
}, this.fetchData);
},
shouldComponentUpdate: function(newProps, newState) {
if (!newState.canvasWidth) {
return false;
}
if (newState.state !== 'newPoints') {
return false;
}
return true;
},
handleFilter: function(thing, i) {
if (this.props.items[i] !== this.state.item) {
this.setState({
item: this.props.items[i],
state: 'newProps'
}, this.fetchData);
}
},
handleClick: function(point) {
var params = {org: this.getParams().org};
params[this.state.item] = point.item;
this.transitionTo(this.state.item, params);
},
handleNewData: function() {
// Group commits by items
var weeksList = _.chain(this.state.rawData)
.pluck('week')
.uniq()
.sort()
.reverse()
.take(this.maxWeeks)
.value();
var counts = _.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;
}, {});
// Extract top items from
var top = _.chain(_.pairs(counts)) // Take [item, count] pairs from counts object
.sortBy(1).reverse() // sort them by count (descending)
.take(this.numElements) // take first N pairs
.pluck(0) // keep only items, omit the counts
.value();
for (var i = top.length; i < this.numElements; i++) {
top[i] = null;
};
var weeks = _.reduce(this.state.rawData, function(res, el) {
if (weeksList.indexOf(el.week) === -1) {
return res;
}
if (res[el.week] === undefined) {
res[el.week] = {};
}
if (top.indexOf(el.item) > -1) {
res[el.week][el.item] = el.commits;
}
return res;
}, {});
var max = _.max(_.map(weeksList, function(week) {
return _.sum(_.values(weeks[week]));
}));
this.setState({
top: top.reverse(),
weeks: weeks,
max: max,
state: 'newPoints'
});
},
buildPathD: function(points) {
var maxWidth = this.state.canvasWidth,
maxHeight = this.height,
maxValue = this.state.max,
len = points.length;
var d = _.map(points, function(point, i) {
return 'L'+ Math.floor(i/(len-1)*maxWidth) +','+ Math.floor(maxHeight - point);
});
d.unshift('M0,'+ maxHeight);
d.push('L'+ maxWidth +','+ maxHeight +'Z');
return d.join(' ');
},
render: function() {
var maxWidth = this.state.canvasWidth,
maxHeight = this.height,
rtop = this.state.top.reverse(),
max = this.state.max;
var points = _.chain(this.state.weeks)
.map(function(items, week) {
var values = _.map(rtop, function(item) {
return items[item] || 0;
});
var sum = 0;
var points = _.map(values, function(val) {
sum += Math.floor(val/max*maxHeight);
return sum;
});
return [week, points];
})
.sort(0)
.reverse()
.take(this.maxWeeks)
.reverse()
.value();
var paths = _.map(rtop, function(item, i) {
var itemPoints = _.map(points, function(pair) {
return pair[1][i];
});
return[item, itemPoints];
});
var colors = {};
var areas = _.map(paths, function(pair, i) {
var item = pair[0], path = pair[1];
if (item !== null) {
colors[item] = Colors2[i];
}
return (
<StackedArea key={'area-'+ i}
item={item}
d={roundPathCorners(this.buildPathD(path), 5)}
color={Colors2[i]} />
);
}.bind(this));
return (
<div className="sachart-container">
<div className="filters">
<Selector thing="item"
items={this.props.items}
value={this.state.item}
onChange={this.handleFilter.bind(this, 'item')} />
<Selector thing="sort"
items={['commits']}
value={'commits'} />
</div>
<svg ref="svg" className="sachart" key="sachart-svg"
width="100%" height={maxHeight}
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ maxHeight}>
{areas.reverse()}
</svg>
<ul className="legend">
{_.pairs(colors).map(function(pair){
return (
<li key={'legend-'+ pair[0]}>
<div className="color-dot" style={{backgroundColor: pair[1]}}></div>
{pair[0]}
</li>
);
})}
</ul>
</div>
);
}
});
var StackedArea = React.createClass({
mixins: [ChartAnimationMixin],
easing: '0.175 0.885 0.32 1.275', // easeOutBack
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"
d={this.state.lastd || this.props.d}
fill={this.props.color}
shapeRendering="optimizeQuality" />
);
}
});