Both charts working great!
This commit is contained in:
parent
dbd943333b
commit
997d77ed00
|
@ -15,8 +15,11 @@
|
||||||
<script src="/bower_components/jquery/dist/jquery.js"></script>
|
<script src="/bower_components/jquery/dist/jquery.js"></script>
|
||||||
<script src="/scripts/colors.js"></script>
|
<script src="/scripts/colors.js"></script>
|
||||||
<script src="/scripts/svground.js"></script>
|
<script src="/scripts/svground.js"></script>
|
||||||
<script src="/scripts/compiled/charts.js"></script>
|
<script src="/scripts/compiled/charts/charts.js"></script>
|
||||||
<script src="/scripts/compiled/bar_chart.js"></script>
|
<script src="/scripts/compiled/charts/data.js"></script>
|
||||||
<script src="/scripts/compiled/stacked_area_chart.js"></script>
|
<script src="/scripts/compiled/charts/animation.js"></script>
|
||||||
|
<script src="/scripts/compiled/charts/selector.js"></script>
|
||||||
|
<script src="/scripts/compiled/charts/bar_chart.js"></script>
|
||||||
|
<script src="/scripts/compiled/charts/stacked_area_chart.js"></script>
|
||||||
<script src="/scripts/compiled/app.js"></script>
|
<script src="/scripts/compiled/app.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,268 +0,0 @@
|
||||||
var BarChart = React.createClass({
|
|
||||||
mixins: [Router.Navigation, Router.State],
|
|
||||||
|
|
||||||
numElements: 15,
|
|
||||||
barHeight: 30,
|
|
||||||
barMargin: 5,
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
currentApi: null,
|
|
||||||
currentParams: null,
|
|
||||||
item: this.props.items[0],
|
|
||||||
sort: 'commits',
|
|
||||||
rawData: [],
|
|
||||||
points: [],
|
|
||||||
oldPoints: [],
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
canvasWidth: 500,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.calculateViewBoxWidth();
|
|
||||||
window.addEventListener('resize', this.calculateViewBoxWidth);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
|
||||||
this.setState({
|
|
||||||
'item': newProps.items[0],
|
|
||||||
'sort': 'commits'
|
|
||||||
}, this.fetchData);
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateViewBoxWidth: function() {
|
|
||||||
this.setState({
|
|
||||||
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFilter: function(thing, i) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleClick: function(point) {
|
|
||||||
var params = {org: this.getParams().org};
|
|
||||||
params[this.state.item] = point.item;
|
|
||||||
this.transitionTo(this.state.item, params);
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchData: function() {
|
|
||||||
if (!this.apiParams().item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.state.currentApi === this.props.api &&
|
|
||||||
this.state.currentParams === JSON.stringify(this.apiParams())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('-----> fetching', this.state.currentApi, this.props.api);
|
|
||||||
this.setState({
|
|
||||||
currentApi: this.props.api,
|
|
||||||
currentParams: JSON.stringify(this.apiParams())
|
|
||||||
}, function() {
|
|
||||||
$.get(this.props.api, this.apiParams(), function(res){
|
|
||||||
this.setState({
|
|
||||||
rawData: res,
|
|
||||||
oldPoints: this.state.points
|
|
||||||
}, this.sort);
|
|
||||||
}.bind(this));
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
sort: function() {
|
|
||||||
console.log('-----> sorting');
|
|
||||||
var sortFun = function(a, b) {
|
|
||||||
return Math.abs(b[this.state.sort]) - Math.abs(a[this.state.sort]);
|
|
||||||
}.bind(this);
|
|
||||||
var points = this.state.rawData.sort(sortFun).slice(0, this.numElements);
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
points: points,
|
|
||||||
min: min,
|
|
||||||
max: max
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
apiParams: function() {
|
|
||||||
var params = _.clone(this.props.params);
|
|
||||||
params['item'] = this.state.item;
|
|
||||||
return params;
|
|
||||||
},
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<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={['commits', 'delta']}
|
|
||||||
value={this.state.sort}
|
|
||||||
onChange={this.handleFilter.bind(this, 'sort')} />
|
|
||||||
</div>
|
|
||||||
<svg ref="svg" className="barchart"
|
|
||||||
width="100%" height={this.height()}
|
|
||||||
viewBox={"0 0 "+ this.state.canvasWidth + " "+ 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.abs(val)/max2*maxWidth,
|
|
||||||
height = this.barHeight,
|
|
||||||
offset = -min/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}
|
|
||||||
onClick={this.handleClick.bind(this, point)} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var Bar = React.createClass({
|
|
||||||
mixins: [Router.Navigation],
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {lastx: 0, lastw: 0};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
|
||||||
console.log("New bar props!", newProps.item, newProps.x, newProps.width);
|
|
||||||
this.setState({
|
|
||||||
lastx: this.props.x,
|
|
||||||
lastw: this.props.width
|
|
||||||
}, this.animate);
|
|
||||||
},
|
|
||||||
|
|
||||||
animate: function() {
|
|
||||||
var bar = this.refs.bar.getDOMNode(),
|
|
||||||
anim = anim = document.createElementNS(SVGNS, 'animate');
|
|
||||||
|
|
||||||
if (bar.childNodes.length > 0) {
|
|
||||||
bar.removeChild(bar.childNodes[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
anim.setAttributeNS(null, 'attributeType', 'XML');
|
|
||||||
anim.setAttributeNS(null, 'attributeName', 'width');
|
|
||||||
anim.setAttributeNS(null, 'from', this.state.lastw);
|
|
||||||
anim.setAttributeNS(null, 'to', this.props.width);
|
|
||||||
anim.setAttributeNS(null, 'dur', '250ms');
|
|
||||||
anim.setAttributeNS(null, 'repeatCount', '1');
|
|
||||||
bar.appendChild(anim);
|
|
||||||
anim.beginElement();
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var val = this.props.value,
|
|
||||||
item = this.props.item,
|
|
||||||
offset = this.props.offset,
|
|
||||||
width = this.props.width,
|
|
||||||
label = item + ': ' + val,
|
|
||||||
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,
|
|
||||||
labelX = 0,
|
|
||||||
labelY = this.props.y + labelOuterHeight + 1, // 1 is magic
|
|
||||||
barX = this.props.x;
|
|
||||||
|
|
||||||
if (labelOffsetWidth <= width) {
|
|
||||||
if (offset > 0) {
|
|
||||||
if (barX === offset) {
|
|
||||||
labelX = barX + 2*labelPaddingH;
|
|
||||||
} else {
|
|
||||||
labelX = barX + width - labelOffsetWidth + 2*labelPaddingH;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
labelX = barX + 2*labelPaddingH;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (barX === offset) {
|
|
||||||
labelX = barX + width + 2*labelPaddingH;
|
|
||||||
} else if (labelOffsetWidth <= barX) {
|
|
||||||
labelX = barX - labelOffsetWidth + 2*labelPaddingH;
|
|
||||||
} else {
|
|
||||||
labelX = barX + width + labelPaddingH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g onClick={this.props.onClick}>
|
|
||||||
<rect ref="bar"
|
|
||||||
className="bar"
|
|
||||||
fill={this.props.color}
|
|
||||||
width={width}
|
|
||||||
height={this.props.height}
|
|
||||||
x={this.props.x}
|
|
||||||
y={this.props.y}
|
|
||||||
rx="2"
|
|
||||||
ry="2" />
|
|
||||||
<rect
|
|
||||||
className="label_underlay"
|
|
||||||
width={labelOuterWidth}
|
|
||||||
height={labelOuterHeight}
|
|
||||||
x={labelX - labelPaddingH}
|
|
||||||
y={this.props.y + labelMarginV}
|
|
||||||
rx="3"
|
|
||||||
ry="3" />
|
|
||||||
<text className="label" x={labelX} y={labelY}>{label}</text>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,89 +0,0 @@
|
||||||
var SVGNS = 'http://www.w3.org/2000/svg',
|
|
||||||
fontFamily = 'Helvetica Neue',
|
|
||||||
fontSize = '16px',
|
|
||||||
Router = ReactRouter;
|
|
||||||
|
|
||||||
var Chart = {
|
|
||||||
calculateViewBoxWidth: function() {
|
|
||||||
this.setState({
|
|
||||||
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
animate: function(ref, attr, from, to) {
|
|
||||||
var node = ref.getDOMNode(),
|
|
||||||
anim = anim = document.createElementNS(SVGNS, 'animate');
|
|
||||||
|
|
||||||
_.map(node.childNodes, function(el) {
|
|
||||||
node.removeChild(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
anim.setAttributeNS(null, 'attributeType', 'XML');
|
|
||||||
anim.setAttributeNS(null, 'attributeName', attr);
|
|
||||||
anim.setAttributeNS(null, 'from', from);
|
|
||||||
anim.setAttributeNS(null, 'to', to);
|
|
||||||
anim.setAttributeNS(null, 'dur', '350ms');
|
|
||||||
// anim.setAttributeNS(null, 'keySplines', [this.easing, this.easing, this.easing].join(';'));
|
|
||||||
anim.setAttributeNS(null, 'repeatCount', '1');
|
|
||||||
node.appendChild(anim);
|
|
||||||
anim.beginElement();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function textWidth(str) {
|
|
||||||
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))
|
|
||||||
text.style.fontFamily = fontFamily;
|
|
||||||
text.style.fontSize = fontSize;
|
|
||||||
|
|
||||||
svg.appendChild(text);
|
|
||||||
document.body.appendChild(svg);
|
|
||||||
var box = text.getBBox();
|
|
||||||
document.body.removeChild(svg);
|
|
||||||
|
|
||||||
return box.width;
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
var StackedAreaChart = React.createClass({
|
var StackedAreaChart = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State, Chart],
|
mixins: [ReactRouter.Navigation, ReactRouter.State, SVGChartMixin, ChartDataMixin],
|
||||||
|
|
||||||
numElements: 10,
|
numElements: 10,
|
||||||
maxWeeks: 20,
|
maxWeeks: 20,
|
||||||
|
@ -7,20 +7,15 @@ var StackedAreaChart = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
currentApi: null,
|
|
||||||
currentParams: null,
|
|
||||||
item: this.props.items[0],
|
item: this.props.items[0],
|
||||||
rawData: [],
|
rawData: [],
|
||||||
top: [],
|
top: [],
|
||||||
max: 1,
|
|
||||||
weeks: [],
|
weeks: [],
|
||||||
canvasWidth: 0,
|
max: 1
|
||||||
state: 'initial'
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.fetchData();
|
|
||||||
this.calculateViewBoxWidth();
|
this.calculateViewBoxWidth();
|
||||||
window.addEventListener('resize', this.calculateViewBoxWidth);
|
window.addEventListener('resize', this.calculateViewBoxWidth);
|
||||||
},
|
},
|
||||||
|
@ -34,14 +29,12 @@ var StackedAreaChart = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(newProps, newState) {
|
shouldComponentUpdate: function(newProps, newState) {
|
||||||
// console.log("Should update?", newState.state);
|
if (!newState.canvasWidth) {
|
||||||
if (newState.canvasWidth === 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (newState.state !== 'newPoints') {
|
if (newState.state !== 'newPoints') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// console.log("Updating!");
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -60,37 +53,7 @@ var StackedAreaChart = React.createClass({
|
||||||
this.transitionTo(this.state.item, params);
|
this.transitionTo(this.state.item, params);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchData: function() {
|
handleNewData: function() {
|
||||||
if (!this.apiParams().item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.state.currentApi === this.props.api &&
|
|
||||||
this.state.currentParams === JSON.stringify(this.apiParams())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('-----> fetching', this.props.api, this.state.item);
|
|
||||||
this.setState({
|
|
||||||
currentApi: this.props.api,
|
|
||||||
currentParams: JSON.stringify(this.apiParams()),
|
|
||||||
state: 'loadingData'
|
|
||||||
}, function() {
|
|
||||||
$.get(this.props.api, this.apiParams(), function(res){
|
|
||||||
this.setState({
|
|
||||||
rawData: res,
|
|
||||||
state: 'newData'
|
|
||||||
}, this.buildPoints);
|
|
||||||
}.bind(this));
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
apiParams: function() {
|
|
||||||
var params = _.clone(this.props.params);
|
|
||||||
params['item'] = this.state.item;
|
|
||||||
return params;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildPoints: function() {
|
|
||||||
// Group commits by items
|
// Group commits by items
|
||||||
var weeksList = _.chain(this.state.rawData)
|
var weeksList = _.chain(this.state.rawData)
|
||||||
.pluck('week')
|
.pluck('week')
|
||||||
|
@ -139,11 +102,10 @@ var StackedAreaChart = React.createClass({
|
||||||
return _.sum(_.values(weeks[week]));
|
return _.sum(_.values(weeks[week]));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
top: top.reverse(),
|
top: top.reverse(),
|
||||||
max: max,
|
|
||||||
weeks: weeks,
|
weeks: weeks,
|
||||||
|
max: max,
|
||||||
state: 'newPoints'
|
state: 'newPoints'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -153,9 +115,10 @@ var StackedAreaChart = React.createClass({
|
||||||
maxHeight = this.height,
|
maxHeight = this.height,
|
||||||
maxValue = this.state.max,
|
maxValue = this.state.max,
|
||||||
len = points.length;
|
len = points.length;
|
||||||
|
|
||||||
var d = _.map(points, function(point, i) {
|
var d = _.map(points, function(point, i) {
|
||||||
return 'L'+ Math.floor(i/len*maxWidth) +','+ Math.floor(maxHeight - point);
|
return 'L'+ Math.floor(i/(len-1)*maxWidth) +','+ Math.floor(maxHeight - point);
|
||||||
});
|
});
|
||||||
d.unshift('M0,'+ maxHeight);
|
d.unshift('M0,'+ maxHeight);
|
||||||
d.push('L'+ maxWidth +','+ maxHeight +'Z');
|
d.push('L'+ maxWidth +','+ maxHeight +'Z');
|
||||||
|
|
||||||
|
@ -163,7 +126,6 @@ var StackedAreaChart = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
// console.log("Rendering!");
|
|
||||||
var maxWidth = this.state.canvasWidth,
|
var maxWidth = this.state.canvasWidth,
|
||||||
maxHeight = this.height,
|
maxHeight = this.height,
|
||||||
rtop = this.state.top.reverse(),
|
rtop = this.state.top.reverse(),
|
||||||
|
@ -189,12 +151,6 @@ var StackedAreaChart = React.createClass({
|
||||||
.reverse()
|
.reverse()
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
var paths = _.reduce(rtop, function(res, item, i) {
|
|
||||||
res[item] = _.map(points, function(pair) {
|
|
||||||
return pair[1][i];
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}, {});
|
|
||||||
var paths = _.map(rtop, function(item, i) {
|
var paths = _.map(rtop, function(item, i) {
|
||||||
var itemPoints = _.map(points, function(pair) {
|
var itemPoints = _.map(points, function(pair) {
|
||||||
return pair[1][i];
|
return pair[1][i];
|
||||||
|
@ -208,7 +164,6 @@ var StackedAreaChart = React.createClass({
|
||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
colors[item] = Colors2[i];
|
colors[item] = Colors2[i];
|
||||||
}
|
}
|
||||||
// console.log("Building path for", item, path);
|
|
||||||
return (
|
return (
|
||||||
<StackedArea key={'area-'+ i}
|
<StackedArea key={'area-'+ i}
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -230,7 +185,7 @@ var StackedAreaChart = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<svg ref="svg" className="sachart" key="sachart-svg"
|
<svg ref="svg" className="sachart" key="sachart-svg"
|
||||||
width="100%" height={maxHeight}
|
width="100%" height={maxHeight}
|
||||||
viewBox={"0 0 "+ this.state.canvasWidth + " "+ maxHeight}>
|
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ maxHeight}>
|
||||||
{areas.reverse()}
|
{areas.reverse()}
|
||||||
</svg>
|
</svg>
|
||||||
<ul className="legend">
|
<ul className="legend">
|
||||||
|
@ -249,33 +204,28 @@ var StackedAreaChart = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
var StackedArea = React.createClass({
|
var StackedArea = React.createClass({
|
||||||
mixins: [Chart],
|
mixins: [ChartAnimationMixin],
|
||||||
easing: '0.55, 0.055, 0.675, 0.19',
|
easing: '0.175 0.885 0.32 1.275', // easeOutBack
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {lastd: ''};
|
return {};
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
// console.log("-- mounted area");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
// console.log("New area props!", newProps.item);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
lastd: this.props.d,
|
lastd: this.props.d || newProps.d,
|
||||||
}, this.state.lastd === '' ? null : this.animateAll);
|
}, this.animateAll);
|
||||||
},
|
},
|
||||||
|
|
||||||
animateAll: function() {
|
animateAll: function() {
|
||||||
// console.log("Animating area", this.props.item);
|
this.clearAnimations(this.refs.path);
|
||||||
this.animate(this.refs.path, 'd', this.state.lastd, this.props.d);
|
this.animate(this.refs.path, 'd', this.state.lastd, this.props.d);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<path ref="path"
|
<path ref="path"
|
||||||
d={this.props.d}
|
d={this.state.lastd || this.props.d}
|
||||||
fill={this.props.color}
|
fill={this.props.color}
|
||||||
shapeRendering="optimizeQuality" />
|
shapeRendering="optimizeQuality" />
|
||||||
);
|
);
|
Loading…
Reference in New Issue