1
0
Fork 0

Intractable stacked area chart

This commit is contained in:
Gregory Eremin 2015-03-16 23:53:54 +07:00
parent 58a9713bf2
commit 299589b9c3
2 changed files with 100 additions and 74 deletions

View File

@ -75,20 +75,14 @@ var StackedAreaChart = React.createClass({
node.className = 'sachart-container focused item-'+ i; node.className = 'sachart-container focused item-'+ i;
}, },
handleFocusOut: function(i) { handleFocusOut: function() {
var node = this.refs.container.getDOMNode(); var node = this.refs.container.getDOMNode();
node.className = 'sachart-container'; node.className = 'sachart-container';
}, },
handleNewData: function() { handleNewData: function() {
// Group commits by items // Group commits by items
var weeksList = _.chain(this.state.rawData) var weeksList = _(this.state.rawData).pluck('week').uniq().sort().reverse().take(this.maxWeeks).value();
.pluck('week')
.uniq()
.sort()
.reverse()
.take(this.maxWeeks)
.value();
var counts = _.reduce(this.state.rawData, function(res, el) { var counts = _.reduce(this.state.rawData, function(res, el) {
if (weeksList.indexOf(el.week) === -1) { if (weeksList.indexOf(el.week) === -1) {
@ -103,7 +97,7 @@ var StackedAreaChart = React.createClass({
}, {}); }, {});
// Extract top items from // Extract top items from
var top = _.chain(_.pairs(counts)) // Take [item, count] pairs from counts object var top = _(_.pairs(counts)) // 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.numElements) // take first N pairs
.pluck(0) // keep only items, omit the counts .pluck(0) // keep only items, omit the counts
@ -125,9 +119,7 @@ var StackedAreaChart = React.createClass({
return res; return res;
}, {}); }, {});
var max = _.max(_.map(weeksList, function(week) { var max = _.max(_.map(weeksList, function(week){ return _.sum(_.values(weeks[week])); }));
return _.sum(_.values(weeks[week]));
}));
this.setState({ this.setState({
top: top, top: top,
@ -141,11 +133,12 @@ var StackedAreaChart = React.createClass({
var maxWidth = this.state.canvasWidth, var maxWidth = this.state.canvasWidth,
maxHeight = this.height; maxHeight = this.height;
var d = _.map(this.buildDots(points), function(dot) { var dots = this.buildDots(points);
return 'L'+ dot[0] +','+ dot[1]; var first = dots.shift();
}); var d = _.map(dots, function(dot){ return 'L'+ dot.x +','+ dot.y; });
d.unshift('M0,'+ maxHeight); d.unshift('M'+ first.x +','+ first.y);
d.push('L'+ maxWidth +','+ maxHeight +'Z'); d.push('L'+ maxWidth +','+ maxHeight);
d.push('L0,'+ maxHeight +' Z');
return d.join(' '); return d.join(' ');
}, },
@ -157,7 +150,21 @@ var StackedAreaChart = React.createClass({
len = points.length; len = points.length;
return _.map(points, function(point, i) { return _.map(points, function(point, i) {
return [Math.floor(i/(len-1)*maxWidth), Math.floor(maxHeight - point)]; point.x = i/(len-1)*maxWidth;
point.y = maxHeight - point.point;
if (point.x < 10) { // Radius
point.x = 10
} else if (point.x > maxWidth - 10) {
point.x = maxWidth - 10;
}
if (point.y < 10) {
point.y = 10;
} else if (point.y > maxHeight - 10) {
point.y = maxHeight - 10;
}
return point;
}); });
}, },
@ -167,7 +174,8 @@ var StackedAreaChart = React.createClass({
top = this.state.top, top = this.state.top,
max = this.state.max; max = this.state.max;
var points = _.chain(this.state.weeks) // [week, [{val, point}, ...]]
var points = _(this.state.weeks)
.map(function(items, week) { .map(function(items, week) {
var values = _.map(top, function(item) { var values = _.map(top, function(item) {
return items[item] || 0; return items[item] || 0;
@ -175,8 +183,11 @@ var StackedAreaChart = React.createClass({
var sum = 0; var sum = 0;
var points = _.map(values, function(val) { var points = _.map(values, function(val) {
sum += Math.floor(val/max*maxHeight*0.96); sum += val/max*maxHeight*0.96;
return sum; return {
val: val,
point: sum
};
}); });
return [week, points]; return [week, points];
@ -187,6 +198,7 @@ var StackedAreaChart = React.createClass({
.reverse() .reverse()
.value(); .value();
// [item, [{val, point}, ...]]
var paths = _.map(top, function(item, i) { var paths = _.map(top, function(item, i) {
var itemPoints = _.map(points, function(pair) { var itemPoints = _.map(points, function(pair) {
return pair[1][i]; return pair[1][i];
@ -194,62 +206,63 @@ var StackedAreaChart = React.createClass({
return[item, itemPoints]; return[item, itemPoints];
}); });
var colors = {};
var areas = _.map(paths, function(pair, i) { var areas = _.map(paths, function(pair, i) {
var item = pair[0], path = pair[1]; var item = pair[0],
if (item !== null) { path = pair[1];
colors[item] = Colors[i];
}
return ( return (
<StackedArea key={'area-'+ i} <StackedArea key={'area-'+ i}
item={item} i={i} item={item} i={i}
d={roundPathCorners(this.buildPathD(path), 3)} d={this.buildPathD(path)}
color={Colors[i]} color={Colors[i]}
onMouseOver={this.handleFocusIn.bind(this, i)} onMouseOver={this.handleFocusIn.bind(this, i)} />
onMouseOut={this.handleFocusOut.bind(this, i)} />
); );
}.bind(this)); }.bind(this));
var dots = _.map(paths, function(pair, i) {
var item = pair[0], path = pair[1];
var dots = this.buildDots(path);
var lastY = 0;
var renderDot = function(dot, j) {
if (lastY === dot[1]) {
return null;
}
lastY = dot[1];
return (
<Dot key={'dot-'+ i +'-'+ j}
item={item} i={i}
value={100}
x={dot[0]}
y={dot[1]} />
);
};
return dots.map(renderDot);
}.bind(this));
var words = this.words, var words = this.words,
who = this.getParams().repo || this.getParams().team || this.getParams().user || this.getParams().org; who = this.getParams().repo ||
this.getParams().team ||
this.getParams().user ||
this.getParams().org;
var params = Object.keys(this.getParams()); var params = Object.keys(this.getParams());
params.splice(params.indexOf('org'), 1); params.splice(params.indexOf('org'), 1);
var subject = params[0]; var subject = params[0];
var renderLegend = function(pair, i){ var renderDot = function(item, i, dot, j) {
if (dot.val === 0) {
return null;
}
return ( return (
<li key={'legend-'+ pair[0]} <Dot key={'dot-'+ i +'-'+ j}
item={item} i={i}
value={dot.val}
x={dot.x}
y={dot.y}
onMouseOver={this.handleFocusIn.bind(this, i)} />
);
}.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){
return (
<li key={'legend-'+ item}
className={'label label-'+ i} className={'label label-'+ i}
onMouseOver={this.handleFocusIn.bind(this, i)} onMouseOver={this.handleFocusIn.bind(this, i)}
onMouseOut={this.handleFocusOut.bind(this, i)}> onMouseOut={this.handleFocusOut.bind(this, i)}>
<div className="color-dot" style={{backgroundColor: pair[1]}}></div> <div className="color-dot" style={{backgroundColor: Colors[i]}}></div>
{pair[0]} {item}
</li> </li>
); );
}.bind(this); }.bind(this);
var legend = _(paths).pluck(0).filter(function(el){ return el !== null; }).value();
return ( return (
<div ref="container" className="sachart-container"> <div ref="container" className="sachart-container">
<div className="whatsgoingon"> <div className="whatsgoingon">
@ -267,13 +280,14 @@ var StackedAreaChart = React.createClass({
onChange={this.handleFilter.bind(this, 'item')} /> onChange={this.handleFilter.bind(this, 'item')} />
</div> </div>
<svg ref="svg" className="sachart" key="sachart-svg" <svg ref="svg" className="sachart" key="sachart-svg"
onMouseOut={this.handleFocusOut}
width="100%" height={maxHeight} width="100%" height={maxHeight}
viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ maxHeight}> viewBox={"0 0 "+ (this.state.canvasWidth || 0) + " "+ maxHeight}>
<g ref="areas">{areas.reverse()}</g> <g ref="areas">{areas.reverse()}</g>
<g ref="dots">{dots}</g> <g ref="dots">{renderedDots}</g>
</svg> </svg>
<ul className="legend"> <ul className="legend">
{_.pairs(colors).map(renderLegend)} {legend.map(renderLegend)}
</ul> </ul>
</div> </div>
); );
@ -305,7 +319,6 @@ var StackedArea = React.createClass({
d={this.state.lastd || this.props.d} d={this.state.lastd || this.props.d}
fill={this.props.color} fill={this.props.color}
onMouseOver={this.props.onMouseOver} onMouseOver={this.props.onMouseOver}
onMouseOut={this.props.onMouseOut}
shapeRendering="optimizeQuality" /> shapeRendering="optimizeQuality" />
); );
} }
@ -314,7 +327,7 @@ var StackedArea = React.createClass({
var Dot = React.createClass({ var Dot = React.createClass({
mixins: [ChartAnimationMixin], mixins: [ChartAnimationMixin],
radius: 12, radius: 10,
getInitialState: function() { getInitialState: function() {
return {}; return {};
@ -333,15 +346,18 @@ var Dot = React.createClass({
render: function() { render: function() {
return ( return (
<g className={'dot dot-'+ this.props.i}> <g className={'dot dot-'+ this.props.i} onMouseOver={this.props.onMouseOver}>
<circle ref="dot" <circle ref="dot"
cx={this.props.x} cx={this.props.x}
cy={this.state.lastY || this.props.y} cy={this.state.lastY || this.props.y}
r={this.radius} r={this.radius}
fill="rgba(255, 255, 255, .9)" /> fill="#fff"
stroke="#f0f0f0"
strokeWidth="1" />
<text ref="value" <text ref="value"
x={this.props.x-8} x={this.props.x}
y={(this.state.lastY || this.props.y)+4}> y={this.props.y+4}
textAnchor="middle">
{this.props.value} {this.props.value}
</text> </text>
</g> </g>

View File

@ -62,23 +62,23 @@
} }
.sachart-container.focused .path { .sachart-container.focused .path {
opacity: 0.03; fill: #f0f0f0;
} }
.sachart-container.focused .label { .sachart-container.focused .label {
opacity: 0.3; opacity: 0.3;
} }
.sachart-container.item-0 .path-0, .sachart-container.item-0 .label-0 { opacity: 1; } .sachart-container.item-0 .path-0 { fill: #EA8369; }
.sachart-container.item-1 .path-1, .sachart-container.item-1 .label-1 { opacity: 1; } .sachart-container.item-1 .path-1 { fill: #FCCD67; }
.sachart-container.item-2 .path-2, .sachart-container.item-2 .label-2 { opacity: 1; } .sachart-container.item-2 .path-2 { fill: #B5A9F2; }
.sachart-container.item-3 .path-3, .sachart-container.item-3 .label-3 { opacity: 1; } .sachart-container.item-3 .path-3 { fill: #9DD7F2; }
.sachart-container.item-4 .path-4, .sachart-container.item-4 .label-4 { opacity: 1; } .sachart-container.item-4 .path-4 { fill: #FFA3A4; }
.sachart-container.item-5 .path-5, .sachart-container.item-5 .label-5 { opacity: 1; } .sachart-container.item-5 .path-5 { fill: #A8EAA8; }
.sachart-container.item-6 .path-6, .sachart-container.item-6 .label-6 { opacity: 1; } .sachart-container.item-6 .path-6 { fill: #F9D08B; }
.sachart-container.item-7 .path-7, .sachart-container.item-7 .label-7 { opacity: 1; } .sachart-container.item-7 .path-7 { fill: #6CCECB; }
.sachart-container.item-8 .path-8, .sachart-container.item-8 .label-8 { opacity: 1; } .sachart-container.item-8 .path-8 { fill: #F9E559; }
.sachart-container.item-9 .path-9, .sachart-container.item-9 .label-9 { opacity: 1; } .sachart-container.item-9 .path-9 { fill: #FFBAD2; }
.sachart-container.item-0 .dot-0 { display: inline; } .sachart-container.item-0 .dot-0 { display: inline; }
.sachart-container.item-1 .dot-1 { display: inline; } .sachart-container.item-1 .dot-1 { display: inline; }
.sachart-container.item-2 .dot-2 { display: inline; } .sachart-container.item-2 .dot-2 { display: inline; }
@ -89,6 +89,16 @@
.sachart-container.item-7 .dot-7 { display: inline; } .sachart-container.item-7 .dot-7 { display: inline; }
.sachart-container.item-8 .dot-8 { display: inline; } .sachart-container.item-8 .dot-8 { display: inline; }
.sachart-container.item-9 .dot-9 { display: inline; } .sachart-container.item-9 .dot-9 { display: inline; }
.sachart-container.item-0 .label-0 { opacity: 1; }
.sachart-container.item-1 .label-1 { opacity: 1; }
.sachart-container.item-2 .label-2 { opacity: 1; }
.sachart-container.item-3 .label-3 { opacity: 1; }
.sachart-container.item-4 .label-4 { opacity: 1; }
.sachart-container.item-5 .label-5 { opacity: 1; }
.sachart-container.item-6 .label-6 { opacity: 1; }
.sachart-container.item-7 .label-7 { opacity: 1; }
.sachart-container.item-8 .label-8 { opacity: 1; }
.sachart-container.item-9 .label-9 { opacity: 1; }
.selector { .selector {