work with both labels and single points

This commit is contained in:
Radek Stepan 2014-09-18 20:55:40 -07:00
parent eff268e1de
commit b364f0cda5
11 changed files with 69 additions and 58 deletions

View File

@ -54,6 +54,7 @@ GitHub Burndown Chart as a service. Public repos are free, for private access au
- [ ] allow people to submit suggestions via GitHub Issues - [ ] allow people to submit suggestions via GitHub Issues
- [ ] find a way where, as a group, we can share repo data by trusting the other repo members that use our platform - [ ] find a way where, as a group, we can share repo data by trusting the other repo members that use our platform
- [ ] support Jira & Gitlab - [ ] support Jira & Gitlab
- [x] closed issues can be moved to a newly created milestone, this messes up the chart since we assume milestone is created first!
## Notes ## Notes

View File

@ -436,7 +436,7 @@ table {
}@keyframes spin{0%{-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)} }@keyframes spin{0%{-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)}
100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-o-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)} 100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-o-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}
} }
#chart{height:200px;position:relative;} #chart{height:300px;position:relative;}
#chart #tooltip{position:absolute;top:0;left:0} #chart #tooltip{position:absolute;top:0;left:0}
#chart svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");} #chart svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");}
#chart svg path.line.actual{stroke:#64584c;stroke-width:3px} #chart svg path.line.actual{stroke:#64584c;stroke-width:3px}

View File

@ -29,7 +29,7 @@
}@keyframes spin{0%{-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)} }@keyframes spin{0%{-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)}
100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-o-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)} 100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-o-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}
} }
#chart{height:200px;position:relative;} #chart{height:300px;position:relative;}
#chart #tooltip{position:absolute;top:0;left:0} #chart #tooltip{position:absolute;top:0;left:0}
#chart svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");} #chart svg path.line{fill:none;stroke-width:1px;clip-path:url("#clip");}
#chart svg path.line.actual{stroke:#64584c;stroke-width:3px} #chart svg path.line.actual{stroke:#64584c;stroke-width:3px}

View File

@ -40038,7 +40038,7 @@ if (typeof exports === 'object') {
"milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"] "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
}, },
"chart": { "chart": {
"off_days": [6, 7], "off_days": [],
"datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/, "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
"size_label": /^size (\d+)$/, "size_label": /^size (\d+)$/,
"location": /^#!((\/[^\/]+){2,3})$/, "location": /^#!((\/[^\/]+){2,3})$/,
@ -40141,8 +40141,8 @@ if (typeof exports === 'object') {
var head, max, min, range, rest; var head, max, min, range, rest;
head = [ head = [
{ {
date: new Date(created_at), 'date': new Date(created_at),
points: total 'points': total
} }
]; ];
min = +Infinity; min = +Infinity;
@ -40156,10 +40156,9 @@ if (typeof exports === 'object') {
if (size > max) { if (size > max) {
max = size; max = size;
} }
return _.extend({}, issue, { issue.date = new Date(closed_at);
date: new Date(closed_at), issue.points = total -= size;
points: total -= size return issue;
});
}); });
range = d3.scale.linear().domain([min, max]).range([5, 8]); range = d3.scale.linear().domain([min, max]).range([5, 8]);
rest = _.map(rest, function(issue) { rest = _.map(rest, function(issue) {
@ -40262,10 +40261,10 @@ if (typeof exports === 'object') {
document.querySelector('#svg').innerHTML = ''; document.querySelector('#svg').innerHTML = '';
_ref = document.querySelector('#chart').getBoundingClientRect(), height = _ref.height, width = _ref.width; _ref = document.querySelector('#chart').getBoundingClientRect(), height = _ref.height, width = _ref.width;
margin = { margin = {
top: 30, 'top': 30,
right: 30, 'right': 30,
bottom: 40, 'bottom': 40,
left: 50 'left': 50
}; };
width -= margin.left + margin.right; width -= margin.left + margin.right;
height -= margin.top + margin.bottom; height -= margin.top + margin.bottom;
@ -40411,7 +40410,7 @@ if (typeof exports === 'object') {
}; };
return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb); return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb);
}, },
'filter': function(collection, regex, cb) { 'filter': function(collection, cb) {
var filtered, total; var filtered, total;
total = 0; total = 0;
switch (config.get('chart.points')) { switch (config.get('chart.points')) {
@ -40430,7 +40429,7 @@ if (typeof exports === 'object') {
} }
issue.size = _.reduce(labels, function(sum, label) { issue.size = _.reduce(labels, function(sum, label) {
var matches; var matches;
if (!(matches = label.name.match(regex))) { if (!(matches = label.name.match(config.get('chart.size_label')))) {
return sum; return sum;
} }
return sum += parseInt(matches[1]); return sum += parseInt(matches[1]);
@ -40531,11 +40530,11 @@ if (typeof exports === 'object') {
return issues.get_all(opts, cb); return issues.get_all(opts, cb);
}, function(all, cb) { }, function(all, cb) {
return async.map(all, function(array, cb) { return async.map(all, function(array, cb) {
return issues.filter(array, opts.size_label, function(err, filtered, total) { return issues.filter(array, function(err, filtered, total) {
return cb(err, [filtered, total]); return cb(err, [filtered, total]);
}); });
}, function(err, _arg) { }, function(err, _arg) {
var closed, open; var closed, open, start;
open = _arg[0], closed = _arg[1]; open = _arg[0], closed = _arg[1];
if (err) { if (err) {
return cb(err); return cb(err);
@ -40544,15 +40543,18 @@ if (typeof exports === 'object') {
return cb('No matching issues found'); return cb('No matching issues found');
} }
opts.issues = { opts.issues = {
closed: { 'closed': {
'points': closed[1], 'points': closed[1],
'data': closed[0] 'data': closed[0]
}, },
open: { 'open': {
'points': open[1], 'points': open[1],
'data': open[0] 'data': open[0]
} }
}; };
if ((start = closed[0][0].closed_at) < opts.milestone.created_at) {
opts.milestone.created_at = start;
}
return cb(null); return cb(null);
}); });
}, function(cb) { }, function(cb) {
@ -40759,7 +40761,7 @@ if (typeof exports === 'object') {
// showChart.mustache // showChart.mustache
root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) { root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) {
module.exports = ["<div id=\"content\" class=\"wrap\">"," <div id=\"chart\">"," <div id=\"tooltip\"></div>"," <div id=\"svg\"></div>"," </div>","</div>"].join("\n"); module.exports = ["<div id=\"content\" class=\"wrap\">"," <div id=\"chart\">"," <div id=\"svg\"></div>"," </div>","</div>"].join("\n");
}); });
// projects.mustache // projects.mustache

View File

@ -72,7 +72,7 @@
"milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"] "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
}, },
"chart": { "chart": {
"off_days": [6, 7], "off_days": [],
"datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/, "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
"size_label": /^size (\d+)$/, "size_label": /^size (\d+)$/,
"location": /^#!((\/[^\/]+){2,3})$/, "location": /^#!((\/[^\/]+){2,3})$/,
@ -175,8 +175,8 @@
var head, max, min, range, rest; var head, max, min, range, rest;
head = [ head = [
{ {
date: new Date(created_at), 'date': new Date(created_at),
points: total 'points': total
} }
]; ];
min = +Infinity; min = +Infinity;
@ -190,10 +190,9 @@
if (size > max) { if (size > max) {
max = size; max = size;
} }
return _.extend({}, issue, { issue.date = new Date(closed_at);
date: new Date(closed_at), issue.points = total -= size;
points: total -= size return issue;
});
}); });
range = d3.scale.linear().domain([min, max]).range([5, 8]); range = d3.scale.linear().domain([min, max]).range([5, 8]);
rest = _.map(rest, function(issue) { rest = _.map(rest, function(issue) {
@ -296,10 +295,10 @@
document.querySelector('#svg').innerHTML = ''; document.querySelector('#svg').innerHTML = '';
_ref = document.querySelector('#chart').getBoundingClientRect(), height = _ref.height, width = _ref.width; _ref = document.querySelector('#chart').getBoundingClientRect(), height = _ref.height, width = _ref.width;
margin = { margin = {
top: 30, 'top': 30,
right: 30, 'right': 30,
bottom: 40, 'bottom': 40,
left: 50 'left': 50
}; };
width -= margin.left + margin.right; width -= margin.left + margin.right;
height -= margin.top + margin.bottom; height -= margin.top + margin.bottom;
@ -445,7 +444,7 @@
}; };
return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb); return async.parallel([_.partial(one_status, 'open'), _.partial(one_status, 'closed')], cb);
}, },
'filter': function(collection, regex, cb) { 'filter': function(collection, cb) {
var filtered, total; var filtered, total;
total = 0; total = 0;
switch (config.get('chart.points')) { switch (config.get('chart.points')) {
@ -464,7 +463,7 @@
} }
issue.size = _.reduce(labels, function(sum, label) { issue.size = _.reduce(labels, function(sum, label) {
var matches; var matches;
if (!(matches = label.name.match(regex))) { if (!(matches = label.name.match(config.get('chart.size_label')))) {
return sum; return sum;
} }
return sum += parseInt(matches[1]); return sum += parseInt(matches[1]);
@ -565,11 +564,11 @@
return issues.get_all(opts, cb); return issues.get_all(opts, cb);
}, function(all, cb) { }, function(all, cb) {
return async.map(all, function(array, cb) { return async.map(all, function(array, cb) {
return issues.filter(array, opts.size_label, function(err, filtered, total) { return issues.filter(array, function(err, filtered, total) {
return cb(err, [filtered, total]); return cb(err, [filtered, total]);
}); });
}, function(err, _arg) { }, function(err, _arg) {
var closed, open; var closed, open, start;
open = _arg[0], closed = _arg[1]; open = _arg[0], closed = _arg[1];
if (err) { if (err) {
return cb(err); return cb(err);
@ -578,15 +577,18 @@
return cb('No matching issues found'); return cb('No matching issues found');
} }
opts.issues = { opts.issues = {
closed: { 'closed': {
'points': closed[1], 'points': closed[1],
'data': closed[0] 'data': closed[0]
}, },
open: { 'open': {
'points': open[1], 'points': open[1],
'data': open[0] 'data': open[0]
} }
}; };
if ((start = closed[0][0].closed_at) < opts.milestone.created_at) {
opts.milestone.created_at = start;
}
return cb(null); return cb(null);
}); });
}, function(cb) { }, function(cb) {
@ -793,7 +795,7 @@
// showChart.mustache // showChart.mustache
root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) { root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) {
module.exports = ["<div id=\"content\" class=\"wrap\">"," <div id=\"chart\">"," <div id=\"tooltip\"></div>"," <div id=\"svg\"></div>"," </div>","</div>"].join("\n"); module.exports = ["<div id=\"content\" class=\"wrap\">"," <div id=\"chart\">"," <div id=\"svg\"></div>"," </div>","</div>"].join("\n");
}); });
// projects.mustache // projects.mustache

View File

@ -21,12 +21,13 @@ module.exports = new Model
] ]
# Chart configuration. # Chart configuration.
"chart": "chart":
"off_days": [ 6, 7 ] # Days we are not working.
"off_days": [ ]
# How do we parse GitHub dates? # How do we parse GitHub dates?
"datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/ "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/
# How does a size label look like? # How does a size label look like?
"size_label": /^size (\d+)$/ "size_label": /^size (\d+)$/
# How do we specify which user/repo/(milestone) we want? # How do we specify which user/repo/(milestone) we want?
"location": /^#!((\/[^\/]+){2,3})$/ "location": /^#!((\/[^\/]+){2,3})$/
# Process all issues as one size. # Process all issues as one size or use labels.
"points": 'ALL_ONE_SIZE' "points": 'ALL_ONE_SIZE'

View File

@ -4,10 +4,13 @@ config = require '../models/config'
module.exports = module.exports =
# A graph of closed issues. # A graph of closed issues.
# `collection`: issues
# `created_at`: milestone start date
# `total`: total number of points (open & closed issues)
'actual': (collection, created_at, total, cb) -> 'actual': (collection, created_at, total, cb) ->
head = [ { head = [ {
date: new Date(created_at) 'date': new Date created_at
points: total 'points': total
} ] } ]
min = +Infinity ; max = -Infinity min = +Infinity ; max = -Infinity
@ -20,9 +23,9 @@ module.exports =
max = size if size > max max = size if size > max
# Dropping points remaining. # Dropping points remaining.
_.extend {}, issue, issue.date = new Date closed_at
date: new Date(closed_at) issue.points = total -= size
points: total -= size issue
# Now add a radius in a range (will be used for a circle). # Now add a radius in a range (will be used for a circle).
range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ]) range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ])
@ -125,14 +128,14 @@ module.exports =
# Get available space. # Get available space.
{ height, width } = document.querySelector('#chart').getBoundingClientRect() { height, width } = document.querySelector('#chart').getBoundingClientRect()
margin = { top: 30, right: 30, bottom: 40, left: 50 } margin = { 'top': 30, 'right': 30, 'bottom': 40, 'left': 50 }
width -= margin.left + margin.right width -= margin.left + margin.right
height -= margin.top + margin.bottom height -= margin.top + margin.bottom
# Scales. # Scales.
x = d3.time.scale().range([ 0, width ]) x = d3.time.scale().range([ 0, width ])
y = d3.scale.linear().range([ height, 0 ]) y = d3.scale.linear().range([ height, 0 ])
# Axes. # Axes.
xAxis = d3.svg.axis().scale(x) xAxis = d3.svg.axis().scale(x)
.orient("bottom") .orient("bottom")
@ -225,9 +228,8 @@ module.exports =
# Show when we closed an issue. # Show when we closed an issue.
svg.selectAll("a.issue") svg.selectAll("a.issue")
.data(actual[1...]) # skip the starting point .data(actual.slice(1)) # skip the starting point
.enter() .enter()
# A wrapping link. # A wrapping link.
.append('svg:a') .append('svg:a')
.attr("xlink:href", ({ html_url }) -> html_url ) .attr("xlink:href", ({ html_url }) -> html_url )

View File

@ -31,10 +31,10 @@ module.exports =
], cb ], cb
# Filter an array of incoming issues based on a regex & save size on them. # Filter an array of incoming issues based on a regex & save size on them.
'filter': (collection, regex, cb) -> 'filter': (collection, cb) ->
# The total size of all issues. # The total size of all issues.
total = 0 total = 0
# Which point counting mode are we in? # Which point counting mode are we in?
switch config.get 'chart.points' switch config.get 'chart.points'
# All issues are the same size # All issues are the same size
@ -53,7 +53,7 @@ module.exports =
# Determine the total issue size from all labels. # Determine the total issue size from all labels.
issue.size = _.reduce labels, (sum, label) -> issue.size = _.reduce labels, (sum, label) ->
# Not matching. # Not matching.
return sum unless matches = label.name.match(regex) return sum unless matches = label.name.match config.get 'chart.size_label'
# Increase sum. # Increase sum.
sum += parseInt matches[1] sum += parseInt matches[1]
, 0 , 0
@ -63,5 +63,5 @@ module.exports =
# Are we saving it? # Are we saving it?
!!issue.size !!issue.size
cb null, filtered, total cb null, filtered, total

View File

@ -21,7 +21,7 @@ module.exports = (opts, cb) ->
# Filter them to labeled ones. # Filter them to labeled ones.
(all, cb) -> (all, cb) ->
async.map all, (array, cb) -> async.map all, (array, cb) ->
issues.filter array, opts.size_label, (err, filtered, total) -> issues.filter array, (err, filtered, total) ->
cb err, [ filtered, total ] cb err, [ filtered, total ]
, (err, [ open, closed ]) -> , (err, [ open, closed ]) ->
return cb err if err return cb err if err
@ -29,8 +29,12 @@ module.exports = (opts, cb) ->
return cb 'No matching issues found' if open[1] + closed[1] is 0 return cb 'No matching issues found' if open[1] + closed[1] is 0
# Save the open/closed on us first. # Save the open/closed on us first.
opts.issues = opts.issues =
closed: { 'points': closed[1], 'data': closed[0] } 'closed': { 'points': closed[1], 'data': closed[0] }
open: { 'points': open[1], 'data': open[0] } 'open': { 'points': open[1], 'data': open[0] }
# Do we need to move the milestone start date?
if (start = closed[0][0].closed_at) < opts.milestone.created_at
opts.milestone.created_at = start
cb null cb null
# Create actual and ideal lines & render. # Create actual and ideal lines & render.

View File

@ -10,7 +10,7 @@ $background2 = #CC9485
// where D3 renders to // where D3 renders to
#chart #chart
height: 200px height: 300px
position: relative position: relative
// position will be adjusted dynamically // position will be adjusted dynamically

View File

@ -1,6 +1,5 @@
<div id="content" class="wrap"> <div id="content" class="wrap">
<div id="chart"> <div id="chart">
<div id="tooltip"></div>
<div id="svg"></div> <div id="svg"></div>
</div> </div>
</div> </div>