Stacked area charts
This commit is contained in:
parent
5f8ace43ef
commit
e58046f227
|
@ -11,8 +11,12 @@
|
||||||
<body></body>
|
<body></body>
|
||||||
<script src="/bower_components/react/react.js"></script>
|
<script src="/bower_components/react/react.js"></script>
|
||||||
<script src="/bower_components/react-router/build/global/ReactRouter.js"></script>
|
<script src="/bower_components/react-router/build/global/ReactRouter.js"></script>
|
||||||
|
<script src="/bower_components/lodash/lodash.js"></script>
|
||||||
<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/charts.js"></script>
|
<script src="/scripts/svground.js"></script>
|
||||||
<script src="/scripts/app.js"></script>
|
<script src="/scripts/compiled/charts.js"></script>
|
||||||
|
<script src="/scripts/compiled/bar_chart.js"></script>
|
||||||
|
<script src="/scripts/compiled/stacked_area_chart.js"></script>
|
||||||
|
<script src="/scripts/compiled/app.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"react": "0.12.2",
|
"react": "0.12.2",
|
||||||
"react-router": "0.12.4",
|
"react-router": "0.12.4",
|
||||||
"jquery": "2.1.3",
|
"jquery": "2.1.3",
|
||||||
|
"lodash": "3.5.0",
|
||||||
"normalize.css": "3.0.2"
|
"normalize.css": "3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,11 +87,14 @@ var Menu = React.createClass({
|
||||||
return (
|
return (
|
||||||
<section className="menu">
|
<section className="menu">
|
||||||
<ul>
|
<ul>
|
||||||
<li key="empact" className="logo-button">
|
<li key="empact">
|
||||||
<Link to="org" params={this.getParams()}>
|
<Link to="org" params={this.getParams()} className="logo-button">
|
||||||
<div className="logo e">e</div>
|
<div className="logo e">e</div>
|
||||||
<div className="logo mp">mp</div>
|
<div className="logo m">m</div>
|
||||||
<div className="logo act">act</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>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li key="orgs-header" className="nav header">Organizations:</li>
|
<li key="orgs-header" className="nav header">Organizations:</li>
|
||||||
|
@ -119,9 +122,18 @@ var OrgStats = React.createClass({
|
||||||
var org = Storage.get('org', this.getParams().org);
|
var org = Storage.get('org', this.getParams().org);
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<InfoBlock image={org.avatar_url} title={org.login} text={org.descr} />
|
<InfoBlock key={'info-block-org-'+ this.getParams().org}
|
||||||
<BarChart key={this.getParams().team} api="/api/stat/orgs/top"
|
image={org.avatar_url}
|
||||||
params={this.getParams()} items={["repo", "team", "user"]} />
|
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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -133,12 +145,18 @@ var TeamStats = React.createClass({
|
||||||
render: function(){
|
render: function(){
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<InfoBlock key={"info-block-"+ this.getParams().team}
|
<InfoBlock key={"info-block-team-"+ this.getParams().team}
|
||||||
image="https://media.licdn.com/mpr/mpr/p/8/005/058/14b/0088c48.jpg"
|
image="https://media.licdn.com/mpr/mpr/p/8/005/058/14b/0088c48.jpg"
|
||||||
title={this.getParams().team}
|
title={this.getParams().team}
|
||||||
text={"The most awesome team in "+ this.getParams().org} />
|
text={"The most awesome team in "+ this.getParams().org} />
|
||||||
<BarChart key={'bar-chart-'+ this.getParams().team} api="/api/stat/teams/top"
|
<BarChart key={'bar-chart-team-'+ this.getParams().team}
|
||||||
params={this.getParams()} items={["repo", "user"]} />
|
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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -149,9 +167,16 @@ var UserStats = React.createClass({
|
||||||
render: function(){
|
render: function(){
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<InfoBlock title={this.getParams().user} />
|
<InfoBlock key={'info-block-user-'+ this.getParams().user}
|
||||||
<BarChart key={'bar-chart-'+ this.getParams().user} api="/api/stat/users/top"
|
title={this.getParams().user} />
|
||||||
params={this.getParams()} items={["repo"]} />
|
<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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -162,9 +187,16 @@ var RepoStats = React.createClass({
|
||||||
render: function(){
|
render: function(){
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<InfoBlock title={this.getParams().repo} />
|
<InfoBlock key={'info-block-repo'+ this.getParams().repo}
|
||||||
<BarChart key={this.getParams().team} api="/api/stat/repos/top"
|
title={this.getParams().repo} />
|
||||||
params={this.getParams()} items={["user", "team"]} />
|
<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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,8 +1,3 @@
|
||||||
var SVGNS = 'http://www.w3.org/2000/svg',
|
|
||||||
fontFamily = 'Helvetica Neue',
|
|
||||||
fontSize = '16px',
|
|
||||||
Router = ReactRouter;
|
|
||||||
|
|
||||||
var BarChart = React.createClass({
|
var BarChart = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State],
|
mixins: [Router.Navigation, Router.State],
|
||||||
|
|
||||||
|
@ -85,8 +80,7 @@ var BarChart = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
apiParams: function() {
|
apiParams: function() {
|
||||||
// Deep copy, but don't use jQuery.extend
|
var params = _.clone(this.props.params);
|
||||||
var params = JSON.parse(JSON.stringify(this.props.params));
|
|
||||||
params['item'] = this.state.item;
|
params['item'] = this.state.item;
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
|
@ -178,7 +172,9 @@ var Bar = React.createClass({
|
||||||
labelX = barX + 2*labelPaddingH;
|
labelX = barX + 2*labelPaddingH;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (labelOffsetWidth <= barX) {
|
if (barX === offset) {
|
||||||
|
labelX = barX + width + 2*labelPaddingH;
|
||||||
|
} else if (labelOffsetWidth <= barX) {
|
||||||
labelX = barX - labelOffsetWidth + 2*labelPaddingH;
|
labelX = barX - labelOffsetWidth + 2*labelPaddingH;
|
||||||
} else {
|
} else {
|
||||||
labelX = barX + width + labelPaddingH;
|
labelX = barX + width + labelPaddingH;
|
||||||
|
@ -199,58 +195,3 @@ var Bar = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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,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" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Source: http://plnkr.co/edit/kGnGGyoOCKil02k04snu?p=info
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* SVG Path Rounding Function *
|
||||||
|
* Copyright (C) 2014 Yona Appletree *
|
||||||
|
* *
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); *
|
||||||
|
* you may not use this file except in compliance with the License. *
|
||||||
|
* You may obtain a copy of the License at *
|
||||||
|
* *
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||||
|
* *
|
||||||
|
* Unless required by applicable law or agreed to in writing, software *
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||||
|
* See the License for the specific language governing permissions and *
|
||||||
|
* limitations under the License. *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG Path rounding function. Takes an input path string and outputs a path
|
||||||
|
* string where all line-line corners have been rounded. Only supports absolute
|
||||||
|
* commands at the moment.
|
||||||
|
*
|
||||||
|
* @param pathString The SVG input path
|
||||||
|
* @param radius The amount to round the corners, either a value in the SVG
|
||||||
|
* coordinate space, or, if useFractionalRadius is true, a value
|
||||||
|
* from 0 to 1.
|
||||||
|
* @param useFractionalRadius If true, the curve radius is expressed as a
|
||||||
|
* fraction of the distance between the point being curved and
|
||||||
|
* the previous and next points.
|
||||||
|
* @returns A new SVG path string with the rounding
|
||||||
|
*/
|
||||||
|
function roundPathCorners(pathString, radius, useFractionalRadius) {
|
||||||
|
function moveTowardsLength(movingPoint, targetPoint, amount) {
|
||||||
|
var width = (targetPoint.x - movingPoint.x);
|
||||||
|
var height = (targetPoint.y - movingPoint.y);
|
||||||
|
|
||||||
|
var distance = Math.sqrt(width*width + height*height);
|
||||||
|
|
||||||
|
return moveTowardsFractional(movingPoint, targetPoint, Math.min(1, amount / distance));
|
||||||
|
}
|
||||||
|
function moveTowardsFractional(movingPoint, targetPoint, fraction) {
|
||||||
|
return {
|
||||||
|
x: movingPoint.x + (targetPoint.x - movingPoint.x)*fraction,
|
||||||
|
y: movingPoint.y + (targetPoint.y - movingPoint.y)*fraction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjusts the ending position of a command
|
||||||
|
function adjustCommand(cmd, newPoint) {
|
||||||
|
if (cmd.length > 2) {
|
||||||
|
cmd[cmd.length - 2] = newPoint.x;
|
||||||
|
cmd[cmd.length - 1] = newPoint.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gives an {x, y} object for a command's ending position
|
||||||
|
function pointForCommand(cmd) {
|
||||||
|
return {
|
||||||
|
x: parseFloat(cmd[cmd.length - 2]),
|
||||||
|
y: parseFloat(cmd[cmd.length - 1]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split apart the path, handing concatonated letters and numbers
|
||||||
|
var pathParts = pathString
|
||||||
|
.split(/[,\s]/)
|
||||||
|
.reduce(function(parts, part){
|
||||||
|
var match = part.match("([a-zA-Z])(.+)");
|
||||||
|
if (match) {
|
||||||
|
parts.push(match[1]);
|
||||||
|
parts.push(match[2]);
|
||||||
|
} else {
|
||||||
|
parts.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Group the commands with their arguments for easier handling
|
||||||
|
var commands = pathParts.reduce(function(commands, part) {
|
||||||
|
if (parseFloat(part) == part && commands.length) {
|
||||||
|
commands[commands.length - 1].push(part);
|
||||||
|
} else {
|
||||||
|
commands.push([part]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// The resulting commands, also grouped
|
||||||
|
var resultCommands = [];
|
||||||
|
|
||||||
|
if (commands.length > 1) {
|
||||||
|
var startPoint = pointForCommand(commands[0]);
|
||||||
|
|
||||||
|
// Handle the close path case with a "virtual" closing line
|
||||||
|
var virtualCloseLine = null;
|
||||||
|
if (commands[commands.length - 1][0] == "Z" && commands[0].length > 2) {
|
||||||
|
virtualCloseLine = ["L", startPoint.x, startPoint.y];
|
||||||
|
commands[commands.length - 1] = virtualCloseLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always use the first command (but it may be mutated)
|
||||||
|
resultCommands.push(commands[0]);
|
||||||
|
|
||||||
|
for (var cmdIndex=1; cmdIndex < commands.length; cmdIndex++) {
|
||||||
|
var prevCmd = resultCommands[resultCommands.length - 1];
|
||||||
|
|
||||||
|
var curCmd = commands[cmdIndex];
|
||||||
|
|
||||||
|
// Handle closing case
|
||||||
|
var nextCmd = (curCmd == virtualCloseLine)
|
||||||
|
? commands[1]
|
||||||
|
: commands[cmdIndex + 1];
|
||||||
|
|
||||||
|
// Nasty logic to decide if this path is a candidite.
|
||||||
|
if (nextCmd && prevCmd && (prevCmd.length > 2) && curCmd[0] == "L" && nextCmd.length > 2 && nextCmd[0] == "L") {
|
||||||
|
// Calc the points we're dealing with
|
||||||
|
var prevPoint = pointForCommand(prevCmd);
|
||||||
|
var curPoint = pointForCommand(curCmd);
|
||||||
|
var nextPoint = pointForCommand(nextCmd);
|
||||||
|
|
||||||
|
// The start and end of the cuve are just our point moved towards the previous and next points, respectivly
|
||||||
|
var curveStart, curveEnd;
|
||||||
|
|
||||||
|
if (useFractionalRadius) {
|
||||||
|
curveStart = moveTowardsFractional(curPoint, prevCmd.origPoint || prevPoint, radius);
|
||||||
|
curveEnd = moveTowardsFractional(curPoint, nextCmd.origPoint || nextPoint, radius);
|
||||||
|
} else {
|
||||||
|
curveStart = moveTowardsLength(curPoint, prevPoint, radius);
|
||||||
|
curveEnd = moveTowardsLength(curPoint, nextPoint, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the current command and add it
|
||||||
|
adjustCommand(curCmd, curveStart);
|
||||||
|
curCmd.origPoint = curPoint;
|
||||||
|
resultCommands.push(curCmd);
|
||||||
|
|
||||||
|
// The curve control points are halfway between the start/end of the curve and
|
||||||
|
// the original point
|
||||||
|
var startControl = moveTowardsFractional(curveStart, curPoint, .5);
|
||||||
|
var endControl = moveTowardsFractional(curPoint, curveEnd, .5);
|
||||||
|
|
||||||
|
// Create the curve
|
||||||
|
var curveCmd = ["C", startControl.x, startControl.y, endControl.x, endControl.y, curveEnd.x, curveEnd.y];
|
||||||
|
// Save the original point for fractional calculations
|
||||||
|
curveCmd.origPoint = curPoint;
|
||||||
|
resultCommands.push(curveCmd);
|
||||||
|
} else {
|
||||||
|
// Pass through commands that don't qualify
|
||||||
|
resultCommands.push(curCmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix up the starting point and restore the close path if the path was orignally closed
|
||||||
|
if (virtualCloseLine) {
|
||||||
|
var newStartPoint = pointForCommand(resultCommands[resultCommands.length-1]);
|
||||||
|
resultCommands.push(["Z"]);
|
||||||
|
adjustCommand(resultCommands[0], newStartPoint);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultCommands = commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultCommands.reduce(function(str, c){ return str + c.join(" ") + " "; }, "");
|
||||||
|
}
|
|
@ -18,6 +18,32 @@
|
||||||
fill: rgba(255, 255, 255, .7);
|
fill: rgba(255, 255, 255, .7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sachart-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
padding: 0 20px 20px 0;
|
||||||
|
}
|
||||||
|
.sachart-container .legend {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.color-dot {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 1px 5px 0 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.sachart-container .legend li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 15px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.logo-button {
|
.logo-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, .6);
|
box-shadow: 1px 1px 1px 0 rgba(0, 0, 0, 0.2), 1px 1px 0 0 #faca50;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
padding: 4px 5px 6px;
|
padding: 4px 5px 6px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .5);
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, .3);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ where
|
||||||
c.week >= :from and
|
c.week >= :from and
|
||||||
c.week <= :to
|
c.week <= :to
|
||||||
group by item, week
|
group by item, week
|
||||||
order by week, commite desc`
|
order by week, commits desc`
|
||||||
|
|
||||||
const teamTopQuery = `
|
const teamTopQuery = `
|
||||||
select
|
select
|
||||||
|
|
Loading…
Reference in New Issue