Stacked area charts
This commit is contained in:
parent
5f8ace43ef
commit
e58046f227
|
@ -11,8 +11,12 @@
|
|||
<body></body>
|
||||
<script src="/bower_components/react/react.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="/scripts/colors.js"></script>
|
||||
<script src="/scripts/charts.js"></script>
|
||||
<script src="/scripts/app.js"></script>
|
||||
<script src="/scripts/svground.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>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"react": "0.12.2",
|
||||
"react-router": "0.12.4",
|
||||
"jquery": "2.1.3",
|
||||
"lodash": "3.5.0",
|
||||
"normalize.css": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,11 +87,14 @@ var Menu = React.createClass({
|
|||
return (
|
||||
<section className="menu">
|
||||
<ul>
|
||||
<li key="empact" className="logo-button">
|
||||
<Link to="org" params={this.getParams()}>
|
||||
<li key="empact">
|
||||
<Link to="org" params={this.getParams()} className="logo-button">
|
||||
<div className="logo e">e</div>
|
||||
<div className="logo mp">mp</div>
|
||||
<div className="logo act">act</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>
|
||||
|
@ -119,9 +122,18 @@ var OrgStats = React.createClass({
|
|||
var org = Storage.get('org', this.getParams().org);
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock image={org.avatar_url} title={org.login} text={org.descr} />
|
||||
<BarChart key={this.getParams().team} api="/api/stat/orgs/top"
|
||||
params={this.getParams()} items={["repo", "team", "user"]} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -133,12 +145,18 @@ var TeamStats = React.createClass({
|
|||
render: function(){
|
||||
return (
|
||||
<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"
|
||||
title={this.getParams().team}
|
||||
text={"The most awesome team in "+ this.getParams().org} />
|
||||
<BarChart key={'bar-chart-'+ this.getParams().team} api="/api/stat/teams/top"
|
||||
params={this.getParams()} items={["repo", "user"]} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -149,9 +167,16 @@ var UserStats = React.createClass({
|
|||
render: function(){
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock title={this.getParams().user} />
|
||||
<BarChart key={'bar-chart-'+ this.getParams().user} api="/api/stat/users/top"
|
||||
params={this.getParams()} items={["repo"]} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -162,9 +187,16 @@ var RepoStats = React.createClass({
|
|||
render: function(){
|
||||
return (
|
||||
<section className="content">
|
||||
<InfoBlock title={this.getParams().repo} />
|
||||
<BarChart key={this.getParams().team} api="/api/stat/repos/top"
|
||||
params={this.getParams()} items={["user", "team"]} />
|
||||
<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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,3 @@
|
|||
var SVGNS = 'http://www.w3.org/2000/svg',
|
||||
fontFamily = 'Helvetica Neue',
|
||||
fontSize = '16px',
|
||||
Router = ReactRouter;
|
||||
|
||||
var BarChart = React.createClass({
|
||||
mixins: [Router.Navigation, Router.State],
|
||||
|
||||
|
@ -85,8 +80,7 @@ var BarChart = React.createClass({
|
|||
},
|
||||
|
||||
apiParams: function() {
|
||||
// Deep copy, but don't use jQuery.extend
|
||||
var params = JSON.parse(JSON.stringify(this.props.params));
|
||||
var params = _.clone(this.props.params);
|
||||
params['item'] = this.state.item;
|
||||
return params;
|
||||
},
|
||||
|
@ -178,7 +172,9 @@ var Bar = React.createClass({
|
|||
labelX = barX + 2*labelPaddingH;
|
||||
}
|
||||
} else {
|
||||
if (labelOffsetWidth <= barX) {
|
||||
if (barX === offset) {
|
||||
labelX = barX + width + 2*labelPaddingH;
|
||||
} else if (labelOffsetWidth <= barX) {
|
||||
labelX = barX - labelOffsetWidth + 2*labelPaddingH;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
.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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.logo-button {
|
||||
display: inline-block;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
|||
font-size: 22px;
|
||||
padding: 4px 5px 6px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ where
|
|||
c.week >= :from and
|
||||
c.week <= :to
|
||||
group by item, week
|
||||
order by week, commite desc`
|
||||
order by week, commits desc`
|
||||
|
||||
const teamTopQuery = `
|
||||
select
|
||||
|
|
Loading…
Reference in New Issue