2015-03-10 07:51:56 +00:00
|
|
|
var SVGNS = 'http://www.w3.org/2000/svg',
|
|
|
|
fontFamily = 'Helvetica Neue',
|
|
|
|
fontSize = '16px',
|
2015-03-08 15:44:48 +00:00
|
|
|
Router = ReactRouter;
|
2015-03-08 10:02:14 +00:00
|
|
|
|
2015-03-07 17:22:32 +00:00
|
|
|
var BarChart = React.createClass({
|
2015-03-09 13:31:51 +00:00
|
|
|
mixins: [Router.Navigation, Router.State],
|
2015-03-09 16:24:33 +00:00
|
|
|
|
2015-03-10 07:51:56 +00:00
|
|
|
numElements: 15,
|
2015-03-09 14:00:06 +00:00
|
|
|
barHeight: 30,
|
2015-03-07 17:22:32 +00:00
|
|
|
barMargin: 5,
|
|
|
|
|
|
|
|
getInitialState: function() {
|
2015-03-09 13:31:51 +00:00
|
|
|
return {
|
|
|
|
item: this.props.items[0],
|
|
|
|
sort: 'commits',
|
|
|
|
rawData: [],
|
|
|
|
points: [],
|
|
|
|
min: 0,
|
2015-03-10 07:51:56 +00:00
|
|
|
max: 1,
|
|
|
|
canvasWidth: 500
|
2015-03-09 13:31:51 +00:00
|
|
|
};
|
2015-03-07 17:22:32 +00:00
|
|
|
},
|
|
|
|
|
2015-03-10 07:51:56 +00:00
|
|
|
calculateViewBoxWidth: function() {
|
|
|
|
this.setState({
|
|
|
|
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2015-03-07 17:22:32 +00:00
|
|
|
componentDidMount: function() {
|
2015-03-09 13:31:51 +00:00
|
|
|
this.fetchData();
|
2015-03-10 07:51:56 +00:00
|
|
|
this.calculateViewBoxWidth();
|
|
|
|
window.addEventListener('resize', this.calculateViewBoxWidth);
|
2015-03-09 13:31:51 +00:00
|
|
|
},
|
|
|
|
|
2015-03-09 15:26:38 +00:00
|
|
|
handleFilter: function(thing, i) {
|
2015-03-09 13:31:51 +00:00
|
|
|
if (thing === 'item' && this.props.items[i] !== this.state.item) {
|
|
|
|
this.setState({
|
|
|
|
item: this.props.items[i]
|
|
|
|
}, this.fetchData);
|
|
|
|
} else if (thing === 'sort' && ['commits', 'delta'][i] !== this.state.sort) {
|
|
|
|
this.setState({
|
|
|
|
sort: ['commits', 'delta'][i]
|
|
|
|
}, this.sort);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-09 15:26:38 +00:00
|
|
|
handleClick: function(point) {
|
|
|
|
var params = {org: this.getParams().org};
|
|
|
|
params[this.state.item] = point.item;
|
|
|
|
this.transitionTo(this.state.item, params);
|
|
|
|
},
|
|
|
|
|
2015-03-09 13:31:51 +00:00
|
|
|
fetchData: function() {
|
|
|
|
$.get(this.props.api, this.apiParams(), function(res){
|
|
|
|
this.setState({
|
|
|
|
rawData: res
|
|
|
|
}, this.sort);
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
sort: function() {
|
|
|
|
var sortFun = function(a, b) {
|
|
|
|
return Math.abs(b[this.state.sort]) - Math.abs(a[this.state.sort]);
|
|
|
|
}.bind(this);
|
2015-03-10 07:51:56 +00:00
|
|
|
var points = this.state.rawData.sort(sortFun).slice(0, this.numElements);
|
2015-03-09 13:31:51 +00:00
|
|
|
|
|
|
|
var min = 0, max = 1;
|
|
|
|
points.map(function(el) {
|
|
|
|
var val = el[this.state.sort];
|
|
|
|
if (val > max) {
|
|
|
|
max = val;
|
|
|
|
}
|
|
|
|
if (val < min) {
|
|
|
|
min = val;
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
|
2015-03-09 16:24:33 +00:00
|
|
|
this.setState({
|
2015-03-09 13:31:51 +00:00
|
|
|
points: points,
|
|
|
|
min: min,
|
|
|
|
max: max
|
2015-03-09 16:24:33 +00:00
|
|
|
});
|
2015-03-09 13:31:51 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
apiParams: function() {
|
2015-03-09 16:24:33 +00:00
|
|
|
// Deep copy, but don't use jQuery.extend
|
2015-03-09 13:31:51 +00:00
|
|
|
var params = JSON.parse(JSON.stringify(this.props.params));
|
|
|
|
params['item'] = this.state.item;
|
|
|
|
return params;
|
2015-03-07 17:22:32 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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() {
|
|
|
|
return (
|
2015-03-09 13:31:51 +00:00
|
|
|
<div className="barchart-container">
|
|
|
|
<div className="filters">
|
|
|
|
<Selector thing="item"
|
|
|
|
items={this.props.items}
|
|
|
|
value={this.state.item}
|
2015-03-09 15:26:38 +00:00
|
|
|
onChange={this.handleFilter.bind(this, 'item')} />
|
2015-03-09 13:31:51 +00:00
|
|
|
<Selector thing="sort"
|
|
|
|
items={['commits', 'delta']}
|
|
|
|
value={this.state.sort}
|
2015-03-09 15:26:38 +00:00
|
|
|
onChange={this.handleFilter.bind(this, 'sort')} />
|
2015-03-09 13:31:51 +00:00
|
|
|
</div>
|
2015-03-10 07:51:56 +00:00
|
|
|
<svg ref="svg" className="barchart"
|
|
|
|
width="100%" height={this.height()}
|
|
|
|
viewBox={"0 0 "+ this.state.canvasWidth + " "+ this.height()}>
|
2015-03-09 13:31:51 +00:00
|
|
|
{this.state.points.map(this.renderBar)}
|
|
|
|
</svg>
|
|
|
|
</div>
|
2015-03-07 17:22:32 +00:00
|
|
|
);
|
2015-03-08 10:02:14 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
renderBar: function(point, i) {
|
2015-03-10 07:51:56 +00:00
|
|
|
var maxWidth = this.state.canvasWidth,
|
2015-03-09 13:31:51 +00:00
|
|
|
val = point[this.state.sort],
|
|
|
|
min = this.state.min,
|
|
|
|
max = this.state.max,
|
|
|
|
max2 = (min < 0 ? max - min : max),
|
|
|
|
width = Math.abs(val)/max2*maxWidth,
|
|
|
|
height = this.barHeight,
|
2015-03-09 14:58:23 +00:00
|
|
|
offset = -min/max2*maxWidth,
|
|
|
|
x = (min >= 0 ? 0 : offset - (val >= 0 ? 0 : width)),
|
2015-03-09 13:31:51 +00:00
|
|
|
y = this.y(i);
|
|
|
|
|
2015-03-08 10:02:14 +00:00
|
|
|
return (
|
2015-03-09 15:26:38 +00:00
|
|
|
<Bar key={point.item} item={point.item} value={val}
|
|
|
|
color={Colors2[i]}
|
2015-03-09 14:58:23 +00:00
|
|
|
x={x} y={y} offset={offset} width={width} height={height}
|
2015-03-09 15:26:38 +00:00
|
|
|
onClick={this.handleClick.bind(this, point)} />
|
2015-03-08 10:02:14 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var Bar = React.createClass({
|
|
|
|
mixins: [Router.Navigation],
|
2015-03-09 14:58:23 +00:00
|
|
|
|
2015-03-08 10:02:14 +00:00
|
|
|
render: function() {
|
2015-03-09 15:26:38 +00:00
|
|
|
var val = this.props.value,
|
|
|
|
item = this.props.item,
|
2015-03-09 14:58:23 +00:00
|
|
|
offset = this.props.offset,
|
|
|
|
width = this.props.width,
|
|
|
|
label = item + ': ' + val,
|
2015-03-10 07:51:56 +00:00
|
|
|
labelPaddingH = 5, // Horizontal
|
|
|
|
labelPaddingV = 2, // Vertical
|
|
|
|
labelWidth = textWidth(label),
|
|
|
|
labelHeight = 16,
|
|
|
|
labelOuterWidth = labelWidth + 2*labelPaddingH,
|
|
|
|
labelOffsetWidth = labelOuterWidth + 2*labelPaddingH,
|
|
|
|
labelOuterHeight = labelHeight + 2*labelPaddingV,
|
|
|
|
labelMarginV = (this.props.height - labelOuterHeight)/2,
|
2015-03-09 14:58:23 +00:00
|
|
|
labelX = 0,
|
2015-03-10 07:51:56 +00:00
|
|
|
labelY = this.props.y + labelOuterHeight + 1, // 1 is magic
|
2015-03-09 14:58:23 +00:00
|
|
|
barX = this.props.x;
|
|
|
|
|
2015-03-10 07:51:56 +00:00
|
|
|
if (labelOffsetWidth <= width) {
|
2015-03-09 14:58:23 +00:00
|
|
|
if (offset > 0) {
|
|
|
|
if (barX === offset) {
|
2015-03-10 07:51:56 +00:00
|
|
|
labelX = barX + 2*labelPaddingH;
|
2015-03-09 14:58:23 +00:00
|
|
|
} else {
|
2015-03-10 07:51:56 +00:00
|
|
|
labelX = barX + width - labelOffsetWidth + 2*labelPaddingH;
|
2015-03-09 14:58:23 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-03-10 07:51:56 +00:00
|
|
|
labelX = barX + 2*labelPaddingH;
|
2015-03-09 14:58:23 +00:00
|
|
|
}
|
|
|
|
} else {
|
2015-03-10 07:51:56 +00:00
|
|
|
if (labelOffsetWidth <= barX) {
|
|
|
|
labelX = barX - labelOffsetWidth + 2*labelPaddingH;
|
2015-03-09 14:58:23 +00:00
|
|
|
} else {
|
2015-03-10 07:51:56 +00:00
|
|
|
labelX = barX + width + labelPaddingH;
|
2015-03-09 14:58:23 +00:00
|
|
|
}
|
2015-03-08 10:02:14 +00:00
|
|
|
}
|
2015-03-09 14:58:23 +00:00
|
|
|
|
2015-03-08 10:02:14 +00:00
|
|
|
return (
|
2015-03-09 15:26:38 +00:00
|
|
|
<g onClick={this.props.onClick}>
|
|
|
|
<rect className="bar" fill={this.props.color}
|
2015-03-09 14:58:23 +00:00
|
|
|
width={width} height={this.props.height}
|
2015-03-09 13:31:51 +00:00
|
|
|
x={this.props.x} y={this.props.y} rx="2" ry="2" />
|
2015-03-08 15:44:48 +00:00
|
|
|
<rect className="label_underlay"
|
2015-03-10 07:51:56 +00:00
|
|
|
x={labelX - labelPaddingH} y={this.props.y + labelMarginV}
|
|
|
|
height={labelOuterHeight} width={labelOuterWidth}
|
2015-03-08 15:44:48 +00:00
|
|
|
rx="3" ry="3" />
|
2015-03-10 07:51:56 +00:00
|
|
|
<text className="label" x={labelX} y={labelY}>{label}</text>
|
2015-03-08 10:02:14 +00:00
|
|
|
</g>
|
|
|
|
);
|
2015-03-07 17:22:32 +00:00
|
|
|
}
|
|
|
|
});
|
2015-03-09 13:31:51 +00:00
|
|
|
|
|
|
|
var Selector = React.createClass({
|
|
|
|
names: {
|
|
|
|
"repo": "Repositories",
|
|
|
|
"team": "Teams",
|
|
|
|
"user": "Users",
|
|
|
|
"commits": "Commits",
|
|
|
|
"delta": "Delta"
|
|
|
|
},
|
|
|
|
|
|
|
|
itemWithName: function(name) {
|
|
|
|
for (item in this.names) {
|
|
|
|
if (this.names[item] === name) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
renderItem: function(item, i) {
|
|
|
|
var itemClass = (item === this.props.value ? 'active' : ''),
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2015-03-09 14:00:06 +00:00
|
|
|
|
2015-03-10 07:51:56 +00:00
|
|
|
function textWidth(str) {
|
2015-03-09 14:00:06 +00:00
|
|
|
var svg = document.createElementNS(SVGNS, "svg");
|
|
|
|
text = document.createElementNS(SVGNS, "text");
|
|
|
|
|
|
|
|
svg.width = 500;
|
|
|
|
svg.height = 500;
|
|
|
|
svg.style.position = 'absolute';
|
|
|
|
svg.style.left = '-1000px';
|
|
|
|
|
|
|
|
text.appendChild(document.createTextNode(str))
|
2015-03-10 07:51:56 +00:00
|
|
|
text.style.fontFamily = fontFamily;
|
|
|
|
text.style.fontSize = fontSize;
|
2015-03-09 14:00:06 +00:00
|
|
|
|
|
|
|
svg.appendChild(text);
|
|
|
|
document.body.appendChild(svg);
|
|
|
|
var box = text.getBBox();
|
|
|
|
document.body.removeChild(svg);
|
|
|
|
|
|
|
|
return box.width;
|
|
|
|
}
|