Barchart filters
This commit is contained in:
parent
2191a0c246
commit
c2fd42bed0
@ -67,11 +67,9 @@ var Dashboard = React.createClass({
|
|||||||
var OrgStats = React.createClass({
|
var OrgStats = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State],
|
mixins: [Router.Navigation, Router.State],
|
||||||
render: function(){
|
render: function(){
|
||||||
var topRepos = "/api/stat/orgs/top?org="+ this.getParams().org +"&item=repo",
|
|
||||||
repoURL = "/app/"+ this.getParams().org +"/repos/";
|
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<BarChart api={topRepos} link={repoURL}/>
|
<BarChart api="/api/stat/orgs/top" params={this.getParams()} items={["repo", "team", "user"]} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,14 +78,9 @@ var OrgStats = React.createClass({
|
|||||||
var TeamStats = React.createClass({
|
var TeamStats = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State],
|
mixins: [Router.Navigation, Router.State],
|
||||||
render: function(){
|
render: function(){
|
||||||
var topRepos = "/api/stat/teams/top"+
|
|
||||||
"?org="+ this.getParams().org +
|
|
||||||
"&team="+ this.getParams().team +
|
|
||||||
"&item=repo",
|
|
||||||
repoURL = "/app/"+ this.getParams().org +"/repos/";
|
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<BarChart api={topRepos} link={repoURL}/>
|
<BarChart api="/api/stat/teams/top" params={this.getParams()} items={["repo", "user"]} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -96,14 +89,9 @@ var TeamStats = React.createClass({
|
|||||||
var UserStats = React.createClass({
|
var UserStats = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State],
|
mixins: [Router.Navigation, Router.State],
|
||||||
render: function(){
|
render: function(){
|
||||||
var topRepos = "/api/stat/users/top"+
|
|
||||||
"?org="+ this.getParams().org +
|
|
||||||
"&author="+ this.getParams().user +
|
|
||||||
"&item=repo",
|
|
||||||
repoURL = "/app/"+ this.getParams().org +"/repos/";
|
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<BarChart api={topRepos} link={repoURL}/>
|
<BarChart api="/api/stat/users/top" params={this.getParams()} items={["repo"]} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -112,14 +100,9 @@ var UserStats = React.createClass({
|
|||||||
var RepoStats = React.createClass({
|
var RepoStats = React.createClass({
|
||||||
mixins: [Router.Navigation, Router.State],
|
mixins: [Router.Navigation, Router.State],
|
||||||
render: function(){
|
render: function(){
|
||||||
var topAuthors = "/api/stat/repos/top"+
|
|
||||||
"?org="+ this.getParams().org +
|
|
||||||
"&repo="+ this.getParams().repo +
|
|
||||||
"&item=author",
|
|
||||||
userURL = "/app/"+ this.getParams().org +"/users/";
|
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<BarChart api={topAuthors} link={userURL}/>
|
<BarChart api="/api/stat/repos/top" params={this.getParams()} items={["team", "user"]} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,77 @@ var SVGNS = "http://www.w3.org/2000/svg",
|
|||||||
Router = ReactRouter;
|
Router = ReactRouter;
|
||||||
|
|
||||||
var BarChart = React.createClass({
|
var BarChart = React.createClass({
|
||||||
|
mixins: [Router.Navigation, Router.State],
|
||||||
barHeight: 40,
|
barHeight: 40,
|
||||||
barMargin: 5,
|
barMargin: 5,
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {points: [], max: 1};
|
return {
|
||||||
|
item: this.props.items[0],
|
||||||
|
sort: 'commits',
|
||||||
|
rawData: [],
|
||||||
|
points: [],
|
||||||
|
min: 0,
|
||||||
|
max: 1
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
$.get(this.props.api, function(res){
|
this.fetchData();
|
||||||
res = res.slice(0, 15);
|
},
|
||||||
var max = 1;
|
|
||||||
res.map(function(el) {
|
onFilter: function(thing, i) {
|
||||||
if (el.commits > max) {
|
if (thing === 'item' && this.props.items[i] !== this.state.item) {
|
||||||
max = el.commits
|
this.setState({
|
||||||
}
|
item: this.props.items[i]
|
||||||
});
|
}, this.fetchData);
|
||||||
this.setState({points: res, max: max});
|
} else if (thing === 'sort' && ['commits', 'delta'][i] !== this.state.sort) {
|
||||||
}.bind(this))
|
this.setState({
|
||||||
|
sort: ['commits', 'delta'][i]
|
||||||
|
}, this.sort);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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, 15);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
s = {
|
||||||
|
points: points,
|
||||||
|
min: min,
|
||||||
|
max: max
|
||||||
|
};
|
||||||
|
console.log(s);
|
||||||
|
this.setState(s);
|
||||||
|
},
|
||||||
|
|
||||||
|
apiParams: function() {
|
||||||
|
// Deep copy
|
||||||
|
// Don't use jQuery.extend
|
||||||
|
var params = JSON.parse(JSON.stringify(this.props.params));
|
||||||
|
params['item'] = this.state.item;
|
||||||
|
return params;
|
||||||
},
|
},
|
||||||
|
|
||||||
height: function() {
|
height: function() {
|
||||||
@ -35,19 +88,43 @@ var BarChart = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
console.log("State:", this.state)
|
||||||
return (
|
return (
|
||||||
<svg className="barchart" width="100%" height={this.height()}>
|
<div className="barchart-container">
|
||||||
{this.state.points.map(this.renderBar)}
|
<div className="filters">
|
||||||
</svg>
|
<Selector thing="item"
|
||||||
|
items={this.props.items}
|
||||||
|
value={this.state.item}
|
||||||
|
onChange={this.onFilter.bind(this, 'item')} />
|
||||||
|
<Selector thing="sort"
|
||||||
|
items={['commits', 'delta']}
|
||||||
|
value={this.state.sort}
|
||||||
|
onChange={this.onFilter.bind(this, 'sort')} />
|
||||||
|
</div>
|
||||||
|
<svg className="barchart" width="100%" height={this.height()}>
|
||||||
|
{this.state.points.map(this.renderBar)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderBar: function(point, i) {
|
renderBar: function(point, i) {
|
||||||
|
var maxWidth = 400,
|
||||||
|
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,
|
||||||
|
x = (min >= 0 ? 0 : -min/max2*maxWidth - (val >= 0 ? 0 : width)),
|
||||||
|
y = this.y(i);
|
||||||
|
console.log(point.item, {val: val, max: max, x: x, y: y, width: width})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bar key={point.item} point={point} i={i} link={this.props.link}
|
<Bar key={point.item} point={point} i={i}
|
||||||
y={this.y(i)}
|
metric={this.state.sort}
|
||||||
width={point.commits/this.state.max}
|
x={x} y={y} width={width} height={height}
|
||||||
height={this.barHeight} />
|
link={this.props.link} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -59,11 +136,12 @@ var Bar = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var p = this.props.point
|
var p = this.props.point,
|
||||||
w = this.props.width*500,
|
val = p[this.props.metric],
|
||||||
label = p.item + ': ' + p.commits,
|
w = this.props.width,
|
||||||
|
label = p.item + ': ' + val,
|
||||||
labelm = 10, // Margin
|
labelm = 10, // Margin
|
||||||
labelw = label.length*9 + 2*labelm, // Width
|
labelw = label.length*9.3 + 2*labelm, // Width
|
||||||
textx = labelm;
|
textx = labelm;
|
||||||
if (labelw + 2*labelm > w) {
|
if (labelw + 2*labelm > w) {
|
||||||
textx = w + textx;
|
textx = w + textx;
|
||||||
@ -71,9 +149,8 @@ var Bar = React.createClass({
|
|||||||
return (
|
return (
|
||||||
<g onClick={this.handleClick}>
|
<g onClick={this.handleClick}>
|
||||||
<rect className="bar" fill={Colors2[this.props.i]}
|
<rect className="bar" fill={Colors2[this.props.i]}
|
||||||
width={this.props.width*500}
|
width={w} height={this.props.height}
|
||||||
height={this.props.height}
|
x={this.props.x} y={this.props.y} rx="2" ry="2" />
|
||||||
x="0" y={this.props.y} rx="2" ry="2" />
|
|
||||||
<rect className="label_underlay"
|
<rect className="label_underlay"
|
||||||
x={textx-6} y={this.props.y+10}
|
x={textx-6} y={this.props.y+10}
|
||||||
height={20} width={labelw}
|
height={20} width={labelw}
|
||||||
@ -83,3 +160,37 @@ 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
.barchart-container {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
.barchart g {
|
.barchart g {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -8,3 +11,34 @@
|
|||||||
.barchart .label_underlay {
|
.barchart .label_underlay {
|
||||||
fill: rgba(255, 255, 255, .8);
|
fill: rgba(255, 255, 255, .8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.filters .item {
|
||||||
|
margin: 0; padding: 0;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.filters .item li {
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
.filters .sort li {
|
||||||
|
margin-left: 0.3em;
|
||||||
|
}
|
||||||
|
.filters .sort {
|
||||||
|
margin: 0; padding: 0;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.filters li {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 2em;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.filters li.active {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #222;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
@ -105,7 +105,7 @@ select
|
|||||||
from contribs c
|
from contribs c
|
||||||
where
|
where
|
||||||
c.owner = :org and
|
c.owner = :org and
|
||||||
c.author = :author and
|
c.author = :user and
|
||||||
c.week >= :from and
|
c.week >= :from and
|
||||||
c.week <= :to
|
c.week <= :to
|
||||||
group by item
|
group by item
|
||||||
@ -120,7 +120,7 @@ select
|
|||||||
from contribs c
|
from contribs c
|
||||||
where
|
where
|
||||||
c.owner = :org and
|
c.owner = :org and
|
||||||
c.author = :author and
|
c.author = :user and
|
||||||
c.week >= :from and
|
c.week >= :from and
|
||||||
c.week <= :to
|
c.week <= :to
|
||||||
group by item
|
group by item
|
||||||
|
@ -19,13 +19,13 @@ type (
|
|||||||
login string
|
login string
|
||||||
}
|
}
|
||||||
statRequest struct {
|
statRequest struct {
|
||||||
Org string `structs:"org"`
|
Org string `structs:"org"`
|
||||||
Team string `structs:"team"`
|
Team string `structs:"team"`
|
||||||
Author string `structs:"author"`
|
User string `structs:"user"`
|
||||||
Repo string `structs:"repo"`
|
Repo string `structs:"repo"`
|
||||||
From int64 `structs:"from"`
|
From int64 `structs:"from"`
|
||||||
To int64 `structs:"to"`
|
To int64 `structs:"to"`
|
||||||
Item string `structs:"item"`
|
Item string `structs:"item"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ func parseStatRequest(r *http.Request) *statRequest {
|
|||||||
|
|
||||||
var item string
|
var item string
|
||||||
switch val := r.FormValue("item"); val {
|
switch val := r.FormValue("item"); val {
|
||||||
case "author":
|
case "author", "user":
|
||||||
item = "c.author"
|
item = "c.author"
|
||||||
case "team":
|
case "team":
|
||||||
item = "t.name"
|
item = "t.name"
|
||||||
@ -85,13 +85,13 @@ func parseStatRequest(r *http.Request) *statRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &statRequest{
|
return &statRequest{
|
||||||
Org: r.FormValue("org"),
|
Org: r.FormValue("org"),
|
||||||
Team: r.FormValue("team"),
|
Team: r.FormValue("team"),
|
||||||
Author: r.FormValue("author"),
|
User: r.FormValue("user"),
|
||||||
Repo: r.FormValue("repo"),
|
Repo: r.FormValue("repo"),
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
Item: item,
|
Item: item,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user