Stacked area charts
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
var Router = ReactRouter,
|
||||
Link = Router.Link;
|
||||
|
||||
var Storage = {
|
||||
set: function(category, key, value) {
|
||||
window.localStorage.setItem(category +'-'+ key, JSON.stringify(value));
|
||||
},
|
||||
|
||||
get: function(category, key) {
|
||||
var val = window.localStorage.getItem(category +'-'+ key);
|
||||
return val === null ? null : JSON.parse(val);
|
||||
}
|
||||
};
|
||||
|
||||
var App = React.createClass({
|
||||
mixins: [Router.Navigation, Router.State],
|
||||
|
||||
orgsURL: "/api/orgs",
|
||||
teamsURL: "/api/teams?org=",
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
orgs: [],
|
||||
org: null,
|
||||
teams: [],
|
||||
team: null
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.loadOrgs();
|
||||
this.loadTeams();
|
||||
},
|
||||
|
||||
loadOrgs: function() {
|
||||
$.get(this.orgsURL, function(res){
|
||||
this.setState({orgs: res});
|
||||
if (res !== null) {
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
var org = res[i];
|
||||
Storage.set('org', org.login, org);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
loadTeams: function() {
|
||||
$.get(this.teamsURL + this.getParams().org, function(res){
|
||||
this.setState({teams: res})
|
||||
if (res !== null) {
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
var team = res[i];
|
||||
Storage.set('team', team.name, team);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
render: function(){
|
||||
return (
|
||||
<section className="app">
|
||||
<Menu orgs={this.state.orgs} teams={this.state.teams} />
|
||||
<Router.RouteHandler />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Menu = React.createClass({
|
||||
mixins: [Router.State],
|
||||
|
||||
render: function() {
|
||||
var renderOrg = function(org) {
|
||||
return (
|
||||
<li key={'org-'+ org.login} className="nav org">
|
||||
<Link to="org" params={{org: org.login}}>{org.login}</Link>
|
||||
</li>
|
||||
)
|
||||
};
|
||||
var renderTeam = function(team) {
|
||||
return (
|
||||
<li key={'team-'+ team.name} className="nav team">
|
||||
<Link to="team" params={{org: team.owner, team: team.name}}>{team.name}</Link>
|
||||
</li>
|
||||
)
|
||||
};
|
||||
return (
|
||||
<section className="menu">
|
||||
<ul>
|
||||
<li key="empact">
|
||||
<Link to="org" params={this.getParams()} className="logo-button">
|
||||
<div className="logo e">e</div>
|
||||
<div className="logo m">m</div>
|
||||
<div className="logo p">p</div>
|
||||
<div className="logo a">a</div>
|
||||
<div className="logo c">c</div>
|
||||
<div className="logo t">t</div>
|
||||
</Link>
|
||||
</li>
|
||||
<li key="orgs-header" className="nav header">Organizations:</li>
|
||||
{this.props.orgs.map(renderOrg)}
|
||||
<li key="teams-header" className="nav header">Teams:</li>
|
||||
{this.props.teams.map(renderTeam)}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Org = React.createClass({
|
||||
render: function(){
|
||||
return (
|
||||
<Router.RouteHandler />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var OrgStats = React.createClass({
|
||||
mixins: [Router.State],
|
||||
|
||||
render: function(){
|
||||
var org = Storage.get('org', this.getParams().org);
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock key={'info-block-org-'+ this.getParams().org}
|
||||
image={org.avatar_url}
|
||||
title={org.login}
|
||||
text={org.descr} />
|
||||
<BarChart key={'bar-chart-'+ this.getParams().org}
|
||||
api="/api/stat/orgs/top"
|
||||
params={this.getParams()}
|
||||
items={["repo", "team", "user"]} />
|
||||
<StackedAreaChart key={'sa-chart-team-'+ this.getParams().team}
|
||||
api="/api/stat/orgs/activity"
|
||||
params={this.getParams()}
|
||||
items={["repo", "team", "user"]} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TeamStats = React.createClass({
|
||||
mixins: [Router.State],
|
||||
|
||||
render: function(){
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock key={"info-block-team-"+ this.getParams().team}
|
||||
image="https://media.licdn.com/mpr/mpr/p/8/005/058/14b/0088c48.jpg"
|
||||
title={this.getParams().team}
|
||||
text={"The most awesome team in "+ this.getParams().org} />
|
||||
<BarChart key={'bar-chart-team-'+ this.getParams().team}
|
||||
api="/api/stat/teams/top"
|
||||
params={this.getParams()}
|
||||
items={["repo", "user"]} />
|
||||
<StackedAreaChart key={'sa-chart-team-'+ this.getParams().team}
|
||||
api="/api/stat/teams/activity"
|
||||
params={this.getParams()}
|
||||
items={["repo", "user"]} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var UserStats = React.createClass({
|
||||
mixins: [Router.State],
|
||||
render: function(){
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock key={'info-block-user-'+ this.getParams().user}
|
||||
title={this.getParams().user} />
|
||||
<BarChart key={'bar-chart-user-'+ this.getParams().user}
|
||||
api="/api/stat/users/top"
|
||||
params={this.getParams()}
|
||||
items={["repo"]} />
|
||||
<StackedAreaChart key={'sa-chart-team-'+ this.getParams().team}
|
||||
api="/api/stat/users/activity"
|
||||
params={this.getParams()}
|
||||
items={["repo"]} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RepoStats = React.createClass({
|
||||
mixins: [Router.State],
|
||||
render: function(){
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock key={'info-block-repo'+ this.getParams().repo}
|
||||
title={this.getParams().repo} />
|
||||
<BarChart key={'bar-chart-repo-'+ this.getParams().team}
|
||||
api="/api/stat/repos/top"
|
||||
params={this.getParams()}
|
||||
items={["user", "team"]} />
|
||||
<StackedAreaChart key={'sa-chart-team-'+ this.getParams().team}
|
||||
api="/api/stat/repos/activity"
|
||||
params={this.getParams()}
|
||||
items={["user", "team"]} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NotFound = React.createClass({
|
||||
render: function(){
|
||||
return (
|
||||
<section className="content">NOT FOUND :(</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SelectOrg = React.createClass({
|
||||
render: function(){
|
||||
return (
|
||||
<section className="content">Please select organization from the menu!</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var InfoBlock = React.createClass({
|
||||
render: function() {
|
||||
var img = <div className="img" style={{backgroundImage: "url("+ this.props.image +")"}} />;
|
||||
return (
|
||||
<div className="info-block">
|
||||
{ this.props.image ? img : null }
|
||||
<h1>{this.props.title}</h1>
|
||||
<p>{this.props.text}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
var routes = [
|
||||
<Router.Route name="root" path="/app/" handler={App}>
|
||||
<Router.DefaultRoute handler={SelectOrg} />
|
||||
<Router.NotFoundRoute handler={NotFound} />
|
||||
<Router.Route name="org" path=":org" handler={Org}>
|
||||
<Router.DefaultRoute handler={OrgStats} />
|
||||
<Router.Route name="team" path="teams/:team" handler={TeamStats} />
|
||||
<Router.Route name="user" path="users/:user" handler={UserStats} />
|
||||
<Router.Route name="repo" path="repos/:repo" handler={RepoStats} />
|
||||
</Router.Route>
|
||||
</Router.Route>
|
||||
];
|
||||
Router.run(routes, Router.HistoryLocation, function(Handler) {
|
||||
React.render(<Handler />, document.body);
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
var BarChart = React.createClass({
|
||||
mixins: [Router.Navigation, Router.State],
|
||||
|
||||
numElements: 15,
|
||||
barHeight: 30,
|
||||
barMargin: 5,
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
item: this.props.items[0],
|
||||
sort: 'commits',
|
||||
rawData: [],
|
||||
points: [],
|
||||
min: 0,
|
||||
max: 1,
|
||||
canvasWidth: 500
|
||||
};
|
||||
},
|
||||
|
||||
calculateViewBoxWidth: function() {
|
||||
this.setState({
|
||||
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.fetchData();
|
||||
this.calculateViewBoxWidth();
|
||||
window.addEventListener('resize', this.calculateViewBoxWidth);
|
||||
},
|
||||
|
||||
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() {
|
||||
$.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);
|
||||
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={point.item} 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],
|
||||
|
||||
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 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"
|
||||
x={labelX - labelPaddingH} y={this.props.y + labelMarginV}
|
||||
height={labelOuterHeight} width={labelOuterWidth}
|
||||
rx="3" ry="3" />
|
||||
<text className="label" x={labelX} y={labelY}>{label}</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
var SVGNS = 'http://www.w3.org/2000/svg',
|
||||
fontFamily = 'Helvetica Neue',
|
||||
fontSize = '16px',
|
||||
Router = ReactRouter;
|
||||
|
||||
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,200 @@
|
||||
var StackedAreaChart = React.createClass({
|
||||
mixins: [Router.Navigation, Router.State],
|
||||
|
||||
numElements: 10,
|
||||
height: 250,
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
item: this.props.items[0],
|
||||
rawData: [],
|
||||
top: [],
|
||||
max: 1,
|
||||
weeks: [],
|
||||
canvasWidth: 500
|
||||
};
|
||||
},
|
||||
|
||||
calculateViewBoxWidth: function() {
|
||||
this.setState({
|
||||
canvasWidth: this.refs.svg.getDOMNode().offsetWidth
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.fetchData();
|
||||
this.calculateViewBoxWidth();
|
||||
window.addEventListener('resize', this.calculateViewBoxWidth);
|
||||
},
|
||||
|
||||
handleFilter: function(thing, i) {
|
||||
if (this.props.items[i] !== this.state.item) {
|
||||
this.setState({
|
||||
item: this.props.items[i]
|
||||
}, this.fetchData);
|
||||
}
|
||||
},
|
||||
|
||||
handleClick: function(point) {
|
||||
var params = {org: this.getParams().org};
|
||||
params[this.state.item] = point.item;
|
||||
this.transitionTo(this.state.item, params);
|
||||
},
|
||||
|
||||
fetchData: function() {
|
||||
$.get(this.props.api, this.apiParams(), function(res){
|
||||
this.setState({
|
||||
rawData: res
|
||||
}, this.buildPoints);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
apiParams: function() {
|
||||
var params = _.clone(this.props.params);
|
||||
params['item'] = this.state.item;
|
||||
return params;
|
||||
},
|
||||
|
||||
buildPoints: function() {
|
||||
// Group commits by items
|
||||
var counts = _.reduce(this.state.rawData, function(res, el) {
|
||||
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();
|
||||
|
||||
var weeks = _.reduce(this.state.rawData, function(res, el) {
|
||||
if (res[el.week] === undefined) {
|
||||
res[el.week] = {};
|
||||
}
|
||||
res[el.week][el.item] = el.commits;
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
var max = _.chain(this.state.rawData)
|
||||
.reduce(function(res, el) {
|
||||
if (res[el.week] === undefined) {
|
||||
res[el.week] = 0;
|
||||
}
|
||||
res[el.week] += el.commits;
|
||||
return res;
|
||||
}, {})
|
||||
.max()
|
||||
.value();
|
||||
|
||||
this.setState({
|
||||
top: top,
|
||||
max: max,
|
||||
weeks: weeks
|
||||
});
|
||||
},
|
||||
|
||||
buildPathD: function(points) {
|
||||
var maxWidth = this.state.canvasWidth,
|
||||
maxHeight = this.height,
|
||||
len = points.length;
|
||||
var d = _.map(points, function(point, i) {
|
||||
return 'L'+ Math.floor(i/len*maxWidth) +','+ (maxHeight - point);
|
||||
});
|
||||
d.unshift('M0,'+maxHeight);
|
||||
d.push('L'+ maxWidth +','+ maxHeight +'Z');
|
||||
|
||||
// for (var i = 0; i < missing; i++) {
|
||||
// d.push('L'+ i +','+ this.props.height/2);
|
||||
// }
|
||||
// for (var i = 0; i < points.length; i++) {
|
||||
// d.push('L'+ missing+i +','+ points[i]);
|
||||
// }
|
||||
// d.push('L'+ this.props.width +','+ this.props.height/2, '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)
|
||||
.value();
|
||||
|
||||
var paths = _.reduce(rtop, function(res, item, i) {
|
||||
res[item] = _.map(points, function(pair) {
|
||||
return pair[1][i];
|
||||
}).slice(-15);
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
var i = -1;
|
||||
var colors = {}
|
||||
var areas = _.map(paths, function(path, item) {
|
||||
i++;
|
||||
colors[item] = Colors2[i];
|
||||
return (
|
||||
<StackedArea key={'sa-item-'+ item}
|
||||
item={item}
|
||||
path={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"
|
||||
width="100%" height={maxHeight}
|
||||
viewBox={"0 0 "+ this.state.canvasWidth + " "+ maxHeight}>
|
||||
{areas.reverse()}
|
||||
</svg>
|
||||
<ul className="legend">
|
||||
{_.pairs(colors).map(function(pair){
|
||||
return (
|
||||
<li><div className="color-dot" style={{backgroundColor: pair[1]}}></div>{pair[0]}</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StackedArea = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<path d={this.props.path} fill={this.props.color} shapeRendering="optimizeQuality" />
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user