prep for chart to be used

This commit is contained in:
Radek Stepan 2014-09-17 20:00:01 -07:00
parent 648725c3dc
commit 21a0e82a85
14 changed files with 11015 additions and 101 deletions

View File

@ -38,6 +38,9 @@ module.exports = (grunt) ->
'vendor/localforage/dist/localforage.js' 'vendor/localforage/dist/localforage.js'
'vendor/async/lib/async.js' 'vendor/async/lib/async.js'
'vendor/moment/moment.js' 'vendor/moment/moment.js'
'vendor/d3/d3.js'
'vendor/d3-tip/index.js'
'vendor/marked/lib/marked.js'
# Our app. # Our app.
'public/js/app.js' 'public/js/app.js'
] ]

View File

@ -6,16 +6,14 @@ GitHub Burndown Chart as a service. Public repos are free, for private access au
## Tasks ## Tasks
### MVP ### MVP - Community Plan
- [x] landing page allows you to immediately jump into action - [x] show a list of projects and their milestones with progress & due date
- [ ] show chart for the current milestone, default to the first one returned and allow to choose a custom one - [ ] show burnchart for that project milestone
- [ ] sort projects based on their closest due dates
- [ ] show only repo name if all projects are under our name
- [ ] show all issues as [one size](https://github.com/radekstepan/github-burndown-chart/issues/46) - [ ] show all issues as [one size](https://github.com/radekstepan/github-burndown-chart/issues/46)
- [x] use local storage to save information about us, but keep the API open for Firebase - [x] use `localStorage` to save project names
### The 20% ### Extras
- [ ] Do not show login/logged-in state when we are still fetching that information from Firebase - [ ] Do not show login/logged-in state when we are still fetching that information from Firebase
- [ ] Handle 404 on routes; from catch all check if '/' or go 404 controller - [ ] Handle 404 on routes; from catch all check if '/' or go 404 controller
@ -32,9 +30,6 @@ GitHub Burndown Chart as a service. Public repos are free, for private access au
- [ ] Since persistence is async, deal with the flicker (show laoding?) when we are still getting data - [ ] Since persistence is async, deal with the flicker (show laoding?) when we are still getting data
- [ ] On page load get all the latest data regardless of `time_ago` - [ ] On page load get all the latest data regardless of `time_ago`
- [ ] Show loading sign on top of [browser window](https://github.com/buunguyen/topbar) which is unobtrusive enough we can show it immediately. - [ ] Show loading sign on top of [browser window](https://github.com/buunguyen/topbar) which is unobtrusive enough we can show it immediately.
### Extras
- [ ] rotate between percentage progress and points left - [ ] rotate between percentage progress and points left
- [ ] be able to config options through UI that currently have to be hardcoded in config - [ ] be able to config options through UI that currently have to be hardcoded in config
- [ ] cache repos in `localStorage` for those that do not use GitHub login - [ ] cache repos in `localStorage` for those that do not use GitHub login

View File

@ -13,6 +13,9 @@
"localforage": "~0.9.2", "localforage": "~0.9.2",
"superagent": "~0.19.0", "superagent": "~0.19.0",
"async": "~0.9.0", "async": "~0.9.0",
"moment": "~2.8.3" "moment": "~2.8.3",
"d3": "~3.4.11",
"d3-tip": "~0.6.5",
"marked": "~0.3.2"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -26,13 +26,17 @@
document.title = 'BurnChart: GitHub Burndown Chart as a Service'; document.title = 'BurnChart: GitHub Burndown Chart as a Service';
Page = require("./views/pages/" + page); Page = require("./views/pages/" + page);
return new Page({ return new Page({
el: el el: el,
'data': {
'route': req.params
}
}); });
}; };
router = { router = {
'': _.partial(route, 'index'), '': _.partial(route, 'index'),
'project/add': _.partial(route, 'addProject'), 'project/add': _.partial(route, 'addProject'),
'chart/:owner/:name/:milestone': _.partial(route, 'showChart'),
'reset': function() { 'reset': function() {
mediator.fire('!projects/clear'); mediator.fire('!projects/clear');
return window.location.hash = '#'; return window.location.hash = '#';
@ -96,36 +100,31 @@
'list': [] 'list': []
}, },
init: function() { init: function() {
var getMilestones, var _this = this;
_this = this;
getMilestones = function() {};
localforage.getItem('projects', function(projects) { localforage.getItem('projects', function(projects) {
if (projects == null) { if (projects == null) {
projects = []; projects = [];
} }
return _this.set('list', projects); return async.each(projects, function(project, cb) {
return mediator.fire('!projects/add', project);
}, function(err) {
if (err) {
throw err;
}
});
}); });
this.observe('list', function(projects) { this.observe('list', function(projects) {
return localforage.setItem('projects', projects); return localforage.setItem('projects', projects);
}); });
mediator.on('!projects/add', function(repo, done) { mediator.on('!projects/add', function(repo, done) {
return request.allMilestones(repo, function(err, res) { return request.allMilestones(repo, function(err, res) {
var active, milestones; var milestones;
if (err) { if (err) {
throw err; return done(err);
} }
milestones = _.pluckMany(res, config.fields.milestone); milestones = _.pluckMany(res, config.fields.milestone);
active = _.find(milestones, function(m) {
return 0 < m.open_issues + m.closed_issues;
});
if (active != null) {
active.active = true;
}
_this.push('list', _.merge(repo, { _this.push('list', _.merge(repo, {
'milestones': { milestones: milestones
'list': milestones,
'checked_at': date.now()
}
})); }));
return done(); return done();
}); });
@ -390,7 +389,7 @@
// layout.mustache // layout.mustache
root.require.register('burnchart/src/templates/layout.js', function(exports, require, module) { root.require.register('burnchart/src/templates/layout.js', function(exports, require, module) {
module.exports = ["<Header/>","","<div id=\"page\">"," <!-- content loaded from a router -->","</div>","","<div id=\"footer\">"," <div class=\"wrap\">"," &copy; 2012-2014 Radek Stepan"," </div>","</div>"].join("\n"); module.exports = ["<Header/>","","<div id=\"page\">"," <!-- content loaded from a router -->","</div>","","<div id=\"footer\">"," <div class=\"wrap\">"," &copy; 2012-2014 <a href=\"http://cloudfi.re\">Cloudfire Systems</a>"," </div>","</div>"].join("\n");
}); });
// addProject.mustache // addProject.mustache
@ -405,10 +404,16 @@
module.exports = ["<div id=\"title\">"," <div class=\"wrap\">"," <h2>Disposable Project</h2>"," <span class=\"milestone\">Milestone 1.0</span>"," <p class=\"description\">The one where we deliver all that we promised.</p>"," </div>","</div>","","<div id=\"content\" class=\"wrap\">"," <Hero/>"," <Projects/>","</div>"].join("\n"); module.exports = ["<div id=\"title\">"," <div class=\"wrap\">"," <h2>Disposable Project</h2>"," <span class=\"milestone\">Milestone 1.0</span>"," <p class=\"description\">The one where we deliver all that we promised.</p>"," </div>","</div>","","<div id=\"content\" class=\"wrap\">"," <Hero/>"," <Projects/>","</div>"].join("\n");
}); });
// showChart.mustache
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");
});
// projects.mustache // projects.mustache
root.require.register('burnchart/src/templates/projects.js', function(exports, require, module) { root.require.register('burnchart/src/templates/projects.js', function(exports, require, module) {
module.exports = ["{{#projects.list}}"," <div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><span class=\"icon sort-alphabet\"></span> Sorted by priority</a>"," <h2>Projects</h2>"," </div>",""," <table>"," {{#projects.list}}"," <tr>"," <td><a class=\"repo\" href=\"#\">{{owner}}/{{name}}</a></td>"," {{# { milestone: getMilestone(milestones.list) } }}"," {{#milestone}}"," <td>"," <span class=\"milestone\">"," {{ milestone.title }}"," <span class=\"icon down-open\">"," </span>"," </td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(format.progress(closed_issues, open_issues))}}%</span>"," <span class=\"due\">due {{format.fromNow(due_on)}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{format.onTime(milestone)}}\" style=\"width:{{format.progress(closed_issues, open_issues)}}%\"></div>"," </div>"," </div>"," </td>"," {{/milestone}}"," {{^milestone}}"," <td colspan=\"2\"><span class=\"milestone\"><em>No milestones yet</em></td>"," {{/milestone}}"," {{/}}"," </tr>"," {{/projects.list}}",""," <tr>"," <td><a class=\"repo\" href=\"#\">radekstepan/disposable</a></td>"," <td><span class=\"milestone\">Milestone 1.0 <span class=\"icon down-open\"></span></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">40%</span>"," <span class=\"due\">due on Friday</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:40%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr class=\"done\">"," <td><a class=\"repo\" href=\"#\">radekstepan/burnchart</a></td>"," <td><span class=\"milestone\">Beta Milestone <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">100%</span>"," <span class=\"due\">due tomorrow</span>"," <div class=\"outer bar\">"," <div class=\"inner bar green\" style=\"width:100%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr>"," <td><a class=\"repo\" href=\"#\">intermine/intermine</a></td>"," <td><span class=\"milestone\">Emma Release 96 <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">27%</span>"," <span class=\"due\">due in 2 weeks</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:27%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr>"," <td><a class=\"repo\" href=\"#\">microsoft/windows</a></td>"," <td><span class=\"milestone\">RC 9 <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">90%</span>"," <span class=\"due red\">overdue by a month</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:90%\"></div>"," </div>"," </div>"," </td>"," </tr>"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><span class=\"icon cog\"></span> Edit</a>"," </div>"," </div>","{{/projects.list}}"].join("\n"); module.exports = ["{{#projects.list.length}}"," <div id=\"projects\">"," <div class=\"header\">"," <a href=\"#\" class=\"sort\"><span class=\"icon sort-alphabet\"></span> Sorted by priority</a>"," <h2>Projects</h2>"," </div>",""," <table>"," {{#projects.list}}"," {{#milestones}}"," <tr>"," <td><a class=\"repo\">{{owner}}/{{name}}</a></td>"," <td>"," <a class=\"milestone\" href=\"#chart/{{owner}}/{{name}}/{{number}}\">{{ title }}</a>"," </td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">{{Math.floor(format.progress(closed_issues, open_issues))}}%</span>"," <span class=\"due\">due {{format.fromNow(due_on)}}</span>"," <div class=\"outer bar\">"," <div class=\"inner bar {{format.onTime(this)}}\" style=\"width:{{format.progress(closed_issues, open_issues)}}%\"></div>"," </div>"," </div>"," </td>"," </tr>"," {{/milestones}}"," {{/projects.list}}",""," <!--"," <tr>"," <td><a class=\"repo\" href=\"#\">radekstepan/disposable</a></td>"," <td><span class=\"milestone\">Milestone 1.0 <span class=\"icon down-open\"></span></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">40%</span>"," <span class=\"due\">due on Friday</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:40%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr class=\"done\">"," <td><a class=\"repo\" href=\"#\">radekstepan/burnchart</a></td>"," <td><span class=\"milestone\">Beta Milestone <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">100%</span>"," <span class=\"due\">due tomorrow</span>"," <div class=\"outer bar\">"," <div class=\"inner bar green\" style=\"width:100%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr>"," <td><a class=\"repo\" href=\"#\">intermine/intermine</a></td>"," <td><span class=\"milestone\">Emma Release 96 <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">27%</span>"," <span class=\"due\">due in 2 weeks</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:27%\"></div>"," </div>"," </div>"," </td>"," </tr>"," <tr>"," <td><a class=\"repo\" href=\"#\">microsoft/windows</a></td>"," <td><span class=\"milestone\">RC 9 <span class=\"icon down-open\"></span></a></td>"," <td>"," <div class=\"progress\">"," <span class=\"percent\">90%</span>"," <span class=\"due red\">overdue by a month</span>"," <div class=\"outer bar\">"," <div class=\"inner bar red\" style=\"width:90%\"></div>"," </div>"," </div>"," </td>"," </tr>"," -->"," </table>",""," <div class=\"footer\">"," <a href=\"#\"><span class=\"icon cog\"></span> Edit</a>"," </div>"," </div>","{{/projects.list}}"].join("\n");
}); });
// date.coffee // date.coffee
@ -583,10 +588,20 @@
Projects: Projects Projects: Projects
}, },
'data': { 'data': {
'format': format, format: format
getMilestone: function(list) { }
return _.findWhere(list, 'active'); });
}
});
// showChart.coffee
root.require.register('burnchart/src/views/pages/showChart.js', function(exports, require, module) {
module.exports = Ractive.extend({
'template': require('../../templates/pages/showChart'),
'adapt': [Ractive.adaptors.Ractive],
init: function() {
return console.log(this.get('route'));
} }
}); });

View File

@ -12,11 +12,12 @@ el = '#page'
route = (page, req, evt) -> route = (page, req, evt) ->
document.title = 'BurnChart: GitHub Burndown Chart as a Service' document.title = 'BurnChart: GitHub Burndown Chart as a Service'
Page = require "./views/pages/#{page}" Page = require "./views/pages/#{page}"
new Page { el } new Page { el, 'data': { 'route': req.params } }
router = router =
'': _.partial route, 'index' '': _.partial route, 'index'
'project/add': _.partial route, 'addProject' 'project/add': _.partial route, 'addProject'
'chart/:owner/:name/:milestone': _.partial route, 'showChart'
# TODO: remove in production. # TODO: remove in production.
'reset': -> 'reset': ->
mediator.fire '!projects/clear' mediator.fire '!projects/clear'

View File

@ -11,14 +11,15 @@ module.exports = new Model
'list': [] 'list': []
init: -> init: ->
# Fetches a list of milestones for a repo.
getMilestones = ->
# Initialize with items stored locally. # Initialize with items stored locally.
localforage.getItem 'projects', (projects=[]) => localforage.getItem 'projects', (projects=[]) =>
@set 'list', projects # Fetch milestones for each of these projects.
async.each projects, (project, cb) ->
mediator.fire '!projects/add', project
, (err) ->
throw err if err
# Persist in local storage. # Persist projects in local storage.
@observe 'list', (projects) -> @observe 'list', (projects) ->
localforage.setItem 'projects', projects localforage.setItem 'projects', projects
@ -28,24 +29,15 @@ module.exports = new Model
# Fetch milestones (which validates repo too). # Fetch milestones (which validates repo too).
request.allMilestones repo, (err, res) => request.allMilestones repo, (err, res) =>
throw err if err return done err if err
# Pluck these fields for milestones. # Pluck these fields for milestones.
milestones = _.pluckMany res, config.fields.milestone milestones = _.pluckMany res, config.fields.milestone
# Set the default milestone as the soonest one with issues. # Push to the stack.
active = _.find milestones, (m) -> @push 'list', _.merge repo, { milestones }
0 < m.open_issues + m.closed_issues
active?.active = true # Call back.
# Push to the stack
@push 'list', _.merge repo,
'milestones':
'list': milestones
'checked_at': do date.now # checked now
# Call back so we can redirect.
do done do done
mediator.on '!projects/clear', => mediator.on '!projects/clear', =>

View File

@ -233,7 +233,7 @@ ul
table table
width: 100% width: 100%
tr tr
td td
background: #FCFCFC background: #FCFCFC
padding: 20px 30px padding: 20px 30px

95
src/styles/chart.styl Normal file
View File

@ -0,0 +1,95 @@
@import 'nib'
// color definitions
$closed = #4DAF7C
$opened = #E55F3A
$grey = #CACACA
$brown = #64584C
$background1 = #D7BCAB
$background2 = #CC9485
// where D3 renders to
#chart
height: 200px
position: relative
// position will be adjusted dynamically
#tooltip
position: absolute
top: 0
left: 0
svg
path
&.line
fill: none
stroke-width: 1px
clip-path: url(#clip)
// actual progress
&.actual
stroke: $brown
stroke-width: 3px
// ideal velocity throughout the sprint
&.ideal
stroke: $grey
stroke-width: 3px
// trend of actual issue closures
&.trendline
stroke: $brown
stroke-width: 1.5px
stroke-dasharray: 5,5
// right now
line
&.today
stroke: $grey
stroke-width: 1px
shape-rendering: crispEdges
stroke-dasharray: 5,5
// represents one issue closed
circle
fill: $brown
// make it easier to click
stroke: transparent
stroke-width: 15px
cursor: pointer
// axes...
.axis
shape-rendering: crispEdges
line
stroke: rgba($grey, 0.25)
shape-rendering: crispEdges
text
font-weight: bold
fill: $grey
path
display: none
// tooltips
.d3-tip
margin-top: -10px
font-size: 11px
padding: 8px 10px 7px 10px
text-align: center
background: rgba(0,0,0,0.75)
color: #fff
border-radius: 3px
&:after
width: 100%
color: rgba(0,0,0,0.8)
content: "\25BC"
position: absolute
&.n:after
margin: -3px 0 0 0
top: 100%
left: 0

View File

@ -6,6 +6,6 @@
<div id="footer"> <div id="footer">
<div class="wrap"> <div class="wrap">
&copy; 2012-2014 Radek Stepan &copy; 2012-2014 <a href="http://cloudfi.re">Cloudfire Systems</a>
</div> </div>
</div> </div>

View File

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

View File

@ -1,4 +1,4 @@
{{#projects.list}} {{#projects.list.length}}
<div id="projects"> <div id="projects">
<div class="header"> <div class="header">
<a href="#" class="sort"><span class="icon sort-alphabet"></span> Sorted by priority</a> <a href="#" class="sort"><span class="icon sort-alphabet"></span> Sorted by priority</a>
@ -7,33 +7,26 @@
<table> <table>
{{#projects.list}} {{#projects.list}}
<tr> {{#milestones}}
<td><a class="repo" href="#">{{owner}}/{{name}}</a></td> <tr>
{{# { milestone: getMilestone(milestones.list) } }} <td><a class="repo">{{owner}}/{{name}}</a></td>
{{#milestone}}
<td> <td>
<span class="milestone"> <a class="milestone" href="#chart/{{owner}}/{{name}}/{{number}}">{{ title }}</a>
{{ milestone.title }}
<span class="icon down-open">
</span>
</td> </td>
<td> <td>
<div class="progress"> <div class="progress">
<span class="percent">{{Math.floor(format.progress(closed_issues, open_issues))}}%</span> <span class="percent">{{Math.floor(format.progress(closed_issues, open_issues))}}%</span>
<span class="due">due {{format.fromNow(due_on)}}</span> <span class="due">due {{format.fromNow(due_on)}}</span>
<div class="outer bar"> <div class="outer bar">
<div class="inner bar {{format.onTime(milestone)}}" style="width:{{format.progress(closed_issues, open_issues)}}%"></div> <div class="inner bar {{format.onTime(this)}}" style="width:{{format.progress(closed_issues, open_issues)}}%"></div>
</div> </div>
</div> </div>
</td> </td>
{{/milestone}} </tr>
{{^milestone}} {{/milestones}}
<td colspan="2"><span class="milestone"><em>No milestones yet</em></td>
{{/milestone}}
{{/}}
</tr>
{{/projects.list}} {{/projects.list}}
<!--
<tr> <tr>
<td><a class="repo" href="#">radekstepan/disposable</a></td> <td><a class="repo" href="#">radekstepan/disposable</a></td>
<td><span class="milestone">Milestone 1.0 <span class="icon down-open"></span></td> <td><span class="milestone">Milestone 1.0 <span class="icon down-open"></span></td>
@ -86,6 +79,7 @@
</div> </div>
</td> </td>
</tr> </tr>
-->
</table> </table>
<div class="footer"> <div class="footer">

View File

@ -8,8 +8,4 @@ module.exports = Ractive.extend
'components': { Hero, Projects } 'components': { Hero, Projects }
'data': 'data': { format }
'format': format
# Find the milestone that is active.
getMilestone: (list) ->
_.findWhere list, 'active'

View File

@ -0,0 +1,8 @@
module.exports = Ractive.extend
'template': require '../../templates/pages/showChart'
'adapt': [ Ractive.adaptors.Ractive ]
init: ->
console.log @get 'route'