showing ideal and actual progress
This commit is contained in:
parent
7aaad2ed8a
commit
dc0f473188
|
@ -3,52 +3,56 @@
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
Rickshaw = require 'rickshaw'
|
Rickshaw = require 'rickshaw'
|
||||||
|
|
||||||
graph = require './graph'
|
# Modules.
|
||||||
|
milestones = require './milestones'
|
||||||
|
issues = require './issues'
|
||||||
|
graph = require './graph'
|
||||||
|
reg = require './regex'
|
||||||
|
|
||||||
templates =
|
# Eco templates as functions.
|
||||||
'body': require './body'
|
templates = {}
|
||||||
'label': require './label'
|
( templates[t] = require("./#{t}") for t in [ 'body', 'label' ] )
|
||||||
|
|
||||||
|
user = 'radekstepan'
|
||||||
|
repo = 'disposable'
|
||||||
|
|
||||||
module.exports = ->
|
module.exports = ->
|
||||||
a = { number: 2, closed_at: '2013-05-09T09:04:53Z', size: 6 }
|
milestones.get_current { user, repo }, (err, warn, m) ->
|
||||||
b = { number: 1, closed_at: '2013-05-20T10:04:53Z', size: 4 }
|
issues.get_all { user, repo, milestone: m.number }, (err, [ open, closed ]) ->
|
||||||
c = { number: 3, closed_at: '2013-06-15T09:04:53Z', size: 2 }
|
issues.filter closed, reg.size_label, (err, warn, closed) ->
|
||||||
|
async.parallel [
|
||||||
|
_.partial(graph.actual, closed, m.created_at, 10)
|
||||||
|
_.partial(graph.ideal, m.created_at, m.due_on, 10)
|
||||||
|
], (err, [ actual, ideal ]) ->
|
||||||
|
document.querySelector('body').innerHTML = templates.body({})
|
||||||
|
|
||||||
async.parallel [
|
graph = new Rickshaw.Graph
|
||||||
_.partial(graph.actual, [ a, b, c ], 20)
|
'element': document.querySelector('#graph')
|
||||||
_.partial(graph.ideal, '2013-05-09T09:04:53Z', '2013-08-29T09:04:53Z', 20)
|
'renderer': 'line'
|
||||||
], (err, [ actual, ideal ]) ->
|
'series': [
|
||||||
throw err if err
|
{ 'data': actual, 'color': '#73C03A', 'name': 'actual' }
|
||||||
|
{ 'data': ideal, 'color': 'rgba(0,0,0,0.2)', 'name': 'ideal' }
|
||||||
|
]
|
||||||
|
|
||||||
document.querySelector('body').innerHTML = templates.body({})
|
hoverDetail = new Rickshaw.Graph.HoverDetail
|
||||||
|
'graph': graph
|
||||||
|
'xFormatter': (timestamp) ->
|
||||||
|
new Date(timestamp * 1e3).toUTCString().substring(0, 11)
|
||||||
|
|
||||||
|
'formatter': (series, timestamp, points) ->
|
||||||
|
templates.label { 'class': series.name, points }
|
||||||
|
|
||||||
|
xAxis = new Rickshaw.Graph.Axis.Time 'graph': graph
|
||||||
|
|
||||||
graph = new Rickshaw.Graph
|
yAxis = new Rickshaw.Graph.Axis.Y
|
||||||
'element': document.querySelector('#graph')
|
'graph': graph
|
||||||
'renderer': 'line'
|
'orientation': 'left'
|
||||||
'series': [
|
'tickFormat': Rickshaw.Fixtures.Number.formatKMBT
|
||||||
{ 'data': actual, 'color': '#73C03A', 'name': 'actual' }
|
|
||||||
{ 'data': ideal, 'color': 'rgba(0,0,0,0.2)', 'name': 'ideal' }
|
|
||||||
]
|
|
||||||
|
|
||||||
hoverDetail = new Rickshaw.Graph.HoverDetail
|
annotator = new Rickshaw.Graph.Annotate
|
||||||
'graph': graph
|
'graph': graph
|
||||||
'xFormatter': (timestamp) ->
|
'element': document.querySelector('#timeline')
|
||||||
new Date(timestamp * 1e3).toUTCString().substring(0, 11)
|
|
||||||
|
annotator.add +new Date / 1e3, 'Now'
|
||||||
'formatter': (series, timestamp, points) ->
|
|
||||||
templates.label { 'class': series.name, points }
|
|
||||||
|
|
||||||
xAxis = new Rickshaw.Graph.Axis.Time 'graph': graph
|
|
||||||
|
|
||||||
yAxis = new Rickshaw.Graph.Axis.Y
|
graph.render()
|
||||||
'graph': graph
|
|
||||||
'orientation': 'left'
|
|
||||||
'tickFormat': Rickshaw.Fixtures.Number.formatKMBT
|
|
||||||
|
|
||||||
annotator = new Rickshaw.Graph.Annotate
|
|
||||||
'graph': graph
|
|
||||||
'element': document.querySelector('#timeline')
|
|
||||||
|
|
||||||
annotator.add +new Date / 1e3, 'Now'
|
|
||||||
|
|
||||||
graph.render()
|
|
|
@ -5,12 +5,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bestiejs/lodash": "*",
|
"bestiejs/lodash": "*",
|
||||||
"caolan/async": "*",
|
"caolan/async": "*",
|
||||||
"cristiandouce/rickshaw": "*"
|
"cristiandouce/rickshaw": "*",
|
||||||
|
"visionmedia/superagent": "*"
|
||||||
},
|
},
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"app.coffee",
|
"app.coffee",
|
||||||
"modules/graph.coffee",
|
"modules/graph.coffee",
|
||||||
"modules/dates.coffee",
|
|
||||||
"modules/issues.coffee",
|
"modules/issues.coffee",
|
||||||
"modules/milestones.coffee",
|
"modules/milestones.coffee",
|
||||||
"modules/regex.coffee",
|
"modules/regex.coffee",
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env coffee
|
|
||||||
{ _ } = require 'lodash'
|
|
||||||
reg = require './regex'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
# Create a range of days between two dates.
|
|
||||||
'range': ({ a, b }, cb) ->
|
|
||||||
# Swap?
|
|
||||||
[ b, a ] = [ a, b ] if b < a
|
|
||||||
|
|
||||||
# When do we start & end?
|
|
||||||
[ year, month, day ] = _.map(a.match(reg.datetime)[1].split('-'), (d) -> parseInt(d) )
|
|
||||||
b = b.match(reg.datetime)[1]
|
|
||||||
|
|
||||||
days = []
|
|
||||||
do add = (i = 0) ->
|
|
||||||
# Use 12 hours to handle daylight saving.
|
|
||||||
days.push c = new Date(year, month - 1, day + i, 12).toJSON().match(reg.datetime)[1]
|
|
||||||
add(i + 1) unless c is b
|
|
||||||
|
|
||||||
cb null, days
|
|
|
@ -1,28 +1,45 @@
|
||||||
#!/usr/bin/env coffee
|
#!/usr/bin/env coffee
|
||||||
{ _ } = require 'lodash'
|
{ _ } = require 'lodash'
|
||||||
|
|
||||||
dates = require './dates'
|
reg = require './regex'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
# Map closed issues ready to be visualized by Rickshaw.
|
# Map closed issues ready to be visualized by Rickshaw.
|
||||||
# Assumes collection has been `filter`ed and is ordered.
|
# Assumes collection has been `filter`ed and is ordered.
|
||||||
'actual': (collection, total, cb) ->
|
'actual': (collection, created_at, total, cb) ->
|
||||||
cb null, _.map collection, ({ closed_at, size }) ->
|
head = [ { x: +new Date(created_at) / 1e3, y: total } ]
|
||||||
|
rest = _.map collection, ({ closed_at, size }) ->
|
||||||
{ x: +new Date(closed_at) / 1e3, y: total -= size }
|
{ x: +new Date(closed_at) / 1e3, y: total -= size }
|
||||||
|
cb null, head.concat rest
|
||||||
|
|
||||||
# Map ideal velocity for each day ready to be visualized by Rickshaw.
|
# Map ideal velocity for each day ready to be visualized by Rickshaw.
|
||||||
'ideal': (a, b, total, cb) ->
|
'ideal': (a, b, total, cb) ->
|
||||||
# Swap?
|
# Swap?
|
||||||
[ b, a ] = [ a, b ] if b < a
|
[ b, a ] = [ a, b ] if b < a
|
||||||
|
|
||||||
# Generate the days in between.
|
# When do we start & end?
|
||||||
dates.range { a, b }, (err, data) ->
|
[ year, month, day ] = _.map(a.match(reg.datetime)[1].split('-'), (d) -> parseInt(d) )
|
||||||
return cb err if err
|
b = b.match(reg.datetime)[1]
|
||||||
|
|
||||||
# Daily velocity needed.
|
# The head/tail are quite specific.
|
||||||
daily = total / data.length
|
head = { x: +new Date(a) / 1e3, y: total }
|
||||||
# Map days to data points.
|
tail = { x: b = +new Date(b) / 1e3, y: 0 }
|
||||||
data = _.map data, (day) ->
|
|
||||||
{ x: +new Date(day) / 1e3, y: total -= daily }
|
|
||||||
|
|
||||||
cb null, data
|
# The fillers...
|
||||||
|
days = []
|
||||||
|
do add = (i = 1) ->
|
||||||
|
# Lunchtime to "handle" daylight saving.
|
||||||
|
c = +new Date year, month - 1, day + i, 12
|
||||||
|
# Add the time point.
|
||||||
|
days.push { x: c / 1e3 }
|
||||||
|
# Moar?
|
||||||
|
add(i + 1) if c < b
|
||||||
|
|
||||||
|
# Daily velocity needed.
|
||||||
|
daily = total / (days.length + 1)
|
||||||
|
# Map points to days.
|
||||||
|
days = _.map days, (day) ->
|
||||||
|
day.y = total -= daily
|
||||||
|
day
|
||||||
|
|
||||||
|
cb null, [ head ].concat(days).concat([ tail ])
|
|
@ -7,16 +7,20 @@ reg = require './regex'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
# Used on an initial fetch of issues for a repo.
|
# Used on an initial fetch of issues for a repo.
|
||||||
'get_all': (opts, cb) ->
|
'get_all': ({ user, repo, milestone }, cb) ->
|
||||||
done = no
|
# For each state...
|
||||||
|
one_status = (state, cb) ->
|
||||||
# For each status...
|
|
||||||
one_status = (status, cb) ->
|
|
||||||
# Concat them here.
|
# Concat them here.
|
||||||
results = []
|
results = []
|
||||||
# One pageful fetch (next pages in series).
|
# One pageful fetch (next pages in series).
|
||||||
do fetch_page = (page = 1) ->
|
do fetch_page = (page = 1) ->
|
||||||
req.all_issues { status: status, page: page }, (err, data) ->
|
req.all_issues {
|
||||||
|
user
|
||||||
|
repo
|
||||||
|
milestone
|
||||||
|
state: state
|
||||||
|
page: page
|
||||||
|
}, (err, data) ->
|
||||||
# Request errors.
|
# Request errors.
|
||||||
return cb err if err
|
return cb err if err
|
||||||
# GitHub errors.
|
# GitHub errors.
|
||||||
|
|
|
@ -11,9 +11,8 @@ module.exports =
|
||||||
return cb data.message if data.message
|
return cb data.message if data.message
|
||||||
# Empty warning.
|
# Empty warning.
|
||||||
return cb null, 'No open milestones for repo' unless data.length
|
return cb null, 'No open milestones for repo' unless data.length
|
||||||
# Find the one due on soonest (string comparison).
|
# The first milestone should be ending soonest.
|
||||||
max = { 'due_on': 'A' }
|
m = data[0]
|
||||||
( max = ms for ms in data when ms.due_on < max.due_on )
|
|
||||||
# Empty milestone?
|
# Empty milestone?
|
||||||
return cb null, 'No issues for milestone' if max.open_issues + max.closed_issues is 0
|
return cb null, 'No issues for milestone' if m.open_issues + m.closed_issues is 0
|
||||||
cb null, null, max
|
cb null, null, m
|
|
@ -1,7 +1,27 @@
|
||||||
#!/usr/bin/env coffee
|
#!/usr/bin/env coffee
|
||||||
|
{ _ } = require 'lodash'
|
||||||
|
|
||||||
|
protocol = 'https'
|
||||||
|
domain = 'api.github.com'
|
||||||
|
token = ''
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
'all_milestones': (opts, cb) ->
|
'all_milestones': ({ user, repo }, cb) ->
|
||||||
cb 'Not implemented'
|
opts = { state: 'open', sort: 'due_date', direction: 'asc' }
|
||||||
'all_issues': (opts, cb) ->
|
request { user, repo, opts, path: 'milestones' }, cb
|
||||||
# { direction: 'asc', per_page: 100 }
|
|
||||||
cb 'Not implemented'
|
'all_issues': ({ user, repo }, cb) ->
|
||||||
|
opts = _.extend {}, arguments[0], { per_page: '100', direction: 'asc' }
|
||||||
|
request { user, repo, opts, path: 'issues' }, cb
|
||||||
|
|
||||||
|
# Make a request using SuperAgent.
|
||||||
|
request = ({ user, repo, path, opts }, cb) ->
|
||||||
|
opts = ( "#{k}=#{v}" for k, v of opts when k not in [ 'user', 'repo' ] ).join('&')
|
||||||
|
|
||||||
|
(require 'superagent')
|
||||||
|
.get("#{protocol}://#{domain}/repos/#{user}/#{repo}/#{path}?#{opts}")
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.set('Accept', 'application/vnd.github.raw')
|
||||||
|
.set('Authorization', "token #{token}")
|
||||||
|
.end (err, data) ->
|
||||||
|
cb err, data?.body
|
|
@ -1,28 +0,0 @@
|
||||||
#!/usr/bin/env coffee
|
|
||||||
assert = require 'assert'
|
|
||||||
path = require 'path'
|
|
||||||
{ _ } = require 'lodash'
|
|
||||||
moment = require 'moment'
|
|
||||||
|
|
||||||
dates = require path.resolve __dirname, '../src/modules/dates.coffee'
|
|
||||||
|
|
||||||
tests =
|
|
||||||
'range between two dates':
|
|
||||||
[ '2013-01-01T00:00:00Z', '2013-01-02T00:00:00Z' ]
|
|
||||||
'range regardless of the order':
|
|
||||||
[ '2013-01-02T00:00:00Z', '2013-01-01T00:00:00Z' ]
|
|
||||||
'range across a year':
|
|
||||||
[ '2012-12-12T00:00:00Z', '2013-01-05T00:00:00Z' ]
|
|
||||||
'range on the same day':
|
|
||||||
[ '2012-12-12T00:00:00Z', '2013-12-12T00:00:00Z' ]
|
|
||||||
'daylight saving':
|
|
||||||
[ '2013-05-09T09:04:53Z', '2013-05-12T09:04:53Z' ]
|
|
||||||
|
|
||||||
for key, value of tests then do (key, value) ->
|
|
||||||
exports[key] = (done) ->
|
|
||||||
[ a, b ] = value
|
|
||||||
dates.range { a, b }, (err, data) ->
|
|
||||||
assert.ifError err
|
|
||||||
assert.equal data.length, Math.abs(moment(a).diff(moment(b), 'days')) + 1
|
|
||||||
_.each data, (date) -> assert moment(date).isValid()
|
|
||||||
done.call null
|
|
|
@ -10,9 +10,10 @@ module.exports =
|
||||||
b = { number: 1, closed_at: '2013-05-09T10:04:53Z', size: 4 }
|
b = { number: 1, closed_at: '2013-05-09T10:04:53Z', size: 4 }
|
||||||
c = { number: 3, closed_at: '2013-05-12T09:04:53Z', size: 2 }
|
c = { number: 3, closed_at: '2013-05-12T09:04:53Z', size: 2 }
|
||||||
|
|
||||||
graph.actual [ a, b, c ], 20, (err, data) ->
|
graph.actual [ a, b, c ], '2013-05-08T09:04:53Z', 20, (err, data) ->
|
||||||
assert.ifError err
|
assert.ifError err
|
||||||
assert.deepEqual data, [
|
assert.deepEqual data, [
|
||||||
|
{ x: 1368003893, y: 20 }
|
||||||
{ x: 1368090293, y: 14 }
|
{ x: 1368090293, y: 14 }
|
||||||
{ x: 1368093893, y: 10 }
|
{ x: 1368093893, y: 10 }
|
||||||
{ x: 1368349493, y: 8 }
|
{ x: 1368349493, y: 8 }
|
||||||
|
|
|
@ -24,19 +24,20 @@ module.exports =
|
||||||
assert.equal milestone.number, 1
|
assert.equal milestone.number, 1
|
||||||
done.call null
|
done.call null
|
||||||
|
|
||||||
|
# We always take from head because of request params.
|
||||||
'get current from > 1': (done) ->
|
'get current from > 1': (done) ->
|
||||||
req.all_milestones = (opts, cb) ->
|
req.all_milestones = (opts, cb) ->
|
||||||
cb null, [
|
cb null, [
|
||||||
{
|
|
||||||
'number': 1
|
|
||||||
'created_at': '2013-01-01T00:00:00Z'
|
|
||||||
'due_on': '2013-02-01T00:00:00Z'
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
'number': 2
|
'number': 2
|
||||||
'created_at': '2013-01-01T00:00:00Z'
|
'created_at': '2013-01-01T00:00:00Z'
|
||||||
'due_on': '2013-01-15T00:00:00Z'
|
'due_on': '2013-01-15T00:00:00Z'
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
'number': 1
|
||||||
|
'created_at': '2013-01-01T00:00:00Z'
|
||||||
|
'due_on': '2013-02-01T00:00:00Z'
|
||||||
|
}
|
||||||
{
|
{
|
||||||
'number': 3
|
'number': 3
|
||||||
'created_at': '2013-01-01T00:00:00Z'
|
'created_at': '2013-01-01T00:00:00Z'
|
||||||
|
|
Loading…
Reference in New Issue