From 9d247206d33dc4237a3039cc7220f414d13f09e7 Mon Sep 17 00:00:00 2001 From: Radek Stepan Date: Thu, 30 Oct 2014 22:12:02 -0600 Subject: [PATCH] using JSON dates internally and moment for parsing --- README.md | 2 +- docs/COVERAGE.html | 2 +- docs/TODO.md | 4 +-- src/models/config.coffee | 2 -- src/modules/chart/lines.coffee | 60 ++++++++++++++++++---------------- src/views/chart.coffee | 16 +++++---- test/lines.coffee | 8 +++-- 7 files changed, 50 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 345fbfb..afd54b5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ GitHub Burndown Chart as a service. Answers the question "are my projects on track"? -[![Build Status](http://img.shields.io/codeship/44524.svg?style=flat)]() +![Build Status](http://img.shields.io/codeship/31951cd0-42c7-0132-d601-5ea438edf284.svg?style=flat) [![Coverage](http://img.shields.io/coveralls/radekstepan/burnchart/master.svg?style=flat)]() [![Dependencies](http://img.shields.io/david/radekstepan/burnchart.svg?style=flat)](https://david-dm.org/radekstepan/burnchart) [![License](http://img.shields.io/badge/license-AGPL--3.0-red.svg?style=flat)](LICENSE) diff --git a/docs/COVERAGE.html b/docs/COVERAGE.html index 168ff7e..c826aec 100644 --- a/docs/COVERAGE.html +++ b/docs/COVERAGE.html @@ -352,4 +352,4 @@ code .string { color: #5890AD } code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

83%
478
399
79

/home/radek/dev/burnchart/src/models/config.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/config',
8 "data": {
9 "firebase": "burnchart",
10 "provider": "github",
11 "fields": {
12 "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
13 },
14 "chart": {
15 "off_days": [],
16 "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/,
17 "size_label": /^size (\d+)$/,
18 "points": 'ONE_SIZE'
19 },
20 "request": {
21 "timeout": 5e3
22 }
23 }
24 });
25
26}).call(this);
27

/home/radek/dev/burnchart/src/models/projects.coffee

80%
122
98
24
LineHitsSource
11(function() {
21 var Model, config, lscache, semver, sortedIndex, stats, user, _,
3 __slice = [].slice;
4
51 _ = require('lodash');
6
71 lscache = require('lscache');
8
91 sortedIndex = require('sortedindex-compare');
10
111 semver = require('semver');
12
131 Model = require('../utils/ractive/model.coffee');
14
151 config = require('../models/config.coffee');
16
171 stats = require('../modules/stats.coffee');
18
191 user = require('./user.coffee');
20
211 module.exports = new Model({
22 'name': 'models/projects',
23 'data': {
24 'sortBy': 'priority',
25 'sortFns': ['progress', 'priority', 'name']
26 },
27 comparator: function() {
2814 var deIdx, defaults, list, sortBy, _ref;
2914 _ref = this.data, list = _ref.list, sortBy = _ref.sortBy;
3014 deIdx = (function(_this) {
3114 return function(fn) {
3214 return function() {
3311 var i, j, rest, _arg;
3411 _arg = arguments[0], rest = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
3511 i = _arg[0], j = _arg[1];
3611 return fn.apply(_this, [[list[i], list[i].milestones[j]]].concat(rest));
37 };
38 };
39 })(this);
4014 defaults = function(arr, hash) {
417 var i, item, k, keys, p, ref, v, _i, _len, _results;
427 _results = [];
437 for (_i = 0, _len = arr.length; _i < _len; _i++) {
4414 item = arr[_i];
4514 _results.push((function() {
4614 var _results1;
4714 _results1 = [];
4814 for (k in hash) {
4926 v = hash[k];
5026 ref = item;
5126 _results1.push((function() {
5226 var _j, _len1, _ref1, _results2;
5326 _ref1 = keys = k.split('.');
5426 _results2 = [];
5526 for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) {
5666 p = _ref1[i];
5766 if (i === keys.length - 1) {
5826 _results2.push(ref[p] != null ? ref[p] : ref[p] = v);
59 } else {
6040 _results2.push(ref = ref[p] != null ? ref[p] : ref[p] = {});
61 }
62 }
6326 return _results2;
64 })());
65 }
6614 return _results1;
67 })());
68 }
697 return _results;
70 };
7114 switch (sortBy) {
72 case 'progress':
732 return deIdx(function(_arg, _arg1) {
741 var aM, aP, bM, bP;
751 aP = _arg[0], aM = _arg[1];
761 bP = _arg1[0], bM = _arg1[1];
771 defaults([aM, bM], {
78 'stats.progress.points': 0
79 });
801 return aM.stats.progress.points - bM.stats.progress.points;
81 });
82 case 'priority':
837 return deIdx(function(_arg, _arg1) {
846 var $a, $b, aM, aP, bM, bP, _ref1;
856 aP = _arg[0], aM = _arg[1];
866 bP = _arg1[0], bM = _arg1[1];
876 defaults([aM, bM], {
88 'stats.progress.time': 0,
89 'stats.days': 1e3
90 });
916 _ref1 = _.map([aM, bM], function(_arg2) {
9212 var stats;
9312 stats = _arg2.stats;
9412 return (stats.progress.points - stats.progress.time) * stats.days;
95 }), $a = _ref1[0], $b = _ref1[1];
966 return $b - $a;
97 });
98 case 'name':
995 return deIdx(function(_arg, _arg1) {
1004 var aM, aP, bM, bP, name, owner;
1014 aP = _arg[0], aM = _arg[1];
1024 bP = _arg1[0], bM = _arg1[1];
1034 if (owner = bP.owner.localeCompare(aP.owner)) {
1040 return owner;
105 }
1064 if (name = bP.name.localeCompare(aP.name)) {
1070 return name;
108 }
1094 if (semver.valid(bM.title) && semver.valid(aM.title)) {
1101 return semver.gt(bM.title, aM.title);
111 } else {
1123 return bM.title.localeCompare(aM.title);
113 }
114 });
115 default:
1160 return function() {
1170 return 0;
118 };
119 }
120 },
121 find: function(project) {
1220 return _.find(this.data.list, project);
123 },
124 exists: function() {
1250 return !!this.find.apply(this, arguments);
126 },
127 add: function(project) {
1280 if (!this.exists(project)) {
1290 return this.push('list', project);
130 }
131 },
132 findIndex: function(_arg) {
13314 var name, owner;
13414 owner = _arg.owner, name = _arg.name;
13514 return _.findIndex(this.data.list, {
136 owner: owner,
137 name: name
138 });
139 },
140 addMilestone: function(project, milestone) {
14114 var i, j;
14214 _.extend(milestone, {
143 'stats': stats(milestone)
144 });
14514 if ((i = this.findIndex(project)) < 0) {
1460 throw 500;
147 }
14814 if (project.milestones != null) {
1498 this.push("list." + i + ".milestones", milestone);
1508 j = this.data.list[i].milestones.length - 1;
151 } else {
1526 this.set("list." + i + ".milestones", [milestone]);
1536 j = 0;
154 }
15514 return this.sort([i, j], [project, milestone]);
156 },
157 saveError: function(project, err) {
1580 var idx;
1590 if ((idx = this.findIndex(project)) > -1) {
1600 if (project.errors != null) {
1610 return this.push("list." + idx + ".errors", err);
162 } else {
1630 return this.set("list." + idx + ".errors", [err]);
164 }
165 } else {
1660 throw 500;
167 }
168 },
169 demo: function() {
1700 return this.set({
171 'list': [
172 {
173 'owner': 'mbostock',
174 'name': 'd3'
175 }, {
176 'owner': 'medic',
177 'name': 'medic-webapp'
178 }, {
179 'owner': 'ractivejs',
180 'name': 'ractive'
181 }, {
182 'owner': 'radekstepan',
183 'name': 'disposable'
184 }, {
185 'owner': 'rails',
186 'name': 'rails'
187 }, {
188 'owner': 'rails',
189 'name': 'spring'
190 }
191 ],
192 'index': []
193 });
194 },
195 clear: function() {
1966 return this.set({
197 'list': [],
198 'index': []
199 });
200 },
201 sort: function(ref, data) {
20217 var i, idx, index, j, m, p, _i, _j, _len, _len1, _ref, _ref1;
20317 index = this.data.index || [];
20417 if (ref) {
20514 idx = sortedIndex(index, data, this.comparator());
20614 index.splice(idx, 0, ref);
207 } else {
2083 _ref = this.data.list;
2093 for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
2100 p = _ref[i];
2110 if (p.milestones == null) {
2120 continue;
213 }
2140 _ref1 = p.milestones;
2150 for (j = _j = 0, _len1 = _ref1.length; _j < _len1; j = ++_j) {
2160 m = _ref1[j];
2170 idx = sortedIndex(index, [p, m], this.comparator());
2180 index.splice(idx, 0, [i, j]);
219 }
220 }
221 }
22217 return this.set('index', index);
223 },
224 onconstruct: function() {
2251 this.subscribe('!projects/add', this.add, this);
2261 return this.subscribe('!projects/demo', this.demo, this);
227 },
228 onrender: function() {
2291 this.set('list', lscache.get('projects') || []);
2301 this.observe('list', function(projects) {
23126 return lscache.set('projects', _.pluckMany(projects, ['owner', 'name']));
232 }, {
233 'init': false
234 });
2351 return this.observe('sortBy', function() {
2363 this.set('index', null);
2373 return this.sort();
238 }, {
239 'init': false
240 });
241 }
242 });
243
244}).call(this);
245

/home/radek/dev/burnchart/src/models/user.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/user'
8 });
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/chart/lines.coffee

94%
88
83
5
LineHitsSource
11(function() {
21 var config, d3, _,
30 __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
4
51 _ = require('lodash');
6
71 d3 = require('d3');
8
91 config = require('../../models/config.coffee');
10
111 module.exports = {
12 actual: function(issues, created_at, total) {
131 var head, max, min, range, rest;
141 head = [
15 {
16 'date': new Date(created_at),
17 'points': total
18 }
19 ];
201 min = +Infinity;
211 max = -Infinity;
221 rest = _.map(issues, function(issue) {
233 var closed_at, size;
243 size = issue.size, closed_at = issue.closed_at;
253 if (size < min) {
263 min = size;
27 }
283 if (size > max) {
291 max = size;
30 }
313 issue.date = new Date(closed_at);
323 issue.points = total -= size;
333 return issue;
34 });
351 range = d3.scale.linear().domain([min, max]).range([5, 8]);
361 rest = _.map(rest, function(issue) {
373 issue.radius = range(issue.size);
383 return issue;
39 });
401 return [].concat(head, rest);
41 },
42 ideal: function(a, b, total) {
431 var cutoff, d, days, length, m, now, once, velocity, y, _ref, _ref1;
441 if (b < a) {
450 _ref = [a, b], b = _ref[0], a = _ref[1];
46 }
471 _ref1 = _.map(a.match(config.data.chart.datetime)[1].split('-'), function(v) {
483 return parseInt(v);
49 }), y = _ref1[0], m = _ref1[1], d = _ref1[2];
501 cutoff = new Date(b);
511 days = [];
521 length = 0;
531 (once = function(inc) {
543 var day, day_of;
553 day = new Date(y, m - 1, d + inc);
563 if (!(day_of = day.getDay())) {
571 day_of = 7;
58 }
593 if (__indexOf.call(config.data.chart.off_days, day_of) >= 0) {
600 days.push({
61 date: day,
62 off_day: true
63 });
64 } else {
653 length += 1;
663 days.push({
67 date: day
68 });
69 }
703 if (!(day > cutoff)) {
712 return once(inc + 1);
72 }
73 })(0);
741 velocity = total / (length - 1);
751 days = _.map(days, function(day, i) {
763 day.points = total;
773 if (days[i] && !days[i].off_day) {
783 total -= velocity;
79 }
803 return day;
81 });
821 if ((now = new Date) > cutoff) {
831 days.push({
84 date: now,
85 points: 0
86 });
87 }
881 return days;
89 },
90 trend: function(actual, created_at, due_on) {
911 var a, b, b1, c1, e, fn, intercept, l, last, now, slope, start, values;
921 if (!actual.length) {
930 return [];
94 }
951 start = +actual[0].date;
961 values = _.map(actual, function(_arg) {
973 var date, points;
983 date = _arg.date, points = _arg.points;
993 return [+date - start, points];
100 });
1011 last = actual[actual.length - 1];
1021 values.push([+(new Date) - start, last.points]);
1031 b1 = 0;
1041 e = 0;
1051 c1 = 0;
1061 a = (l = values.length) * _.reduce(values, function(sum, _arg) {
1074 var a, b;
1084 a = _arg[0], b = _arg[1];
1094 b1 += a;
1104 e += b;
1114 c1 += Math.pow(a, 2);
1124 return sum + (a * b);
113 }, 0);
1141 slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
1151 intercept = (e - (slope * b1)) / l;
1161 fn = function(x) {
1172 return slope * x + intercept;
118 };
1191 created_at = new Date(created_at);
1201 now = new Date;
1211 if (due_on) {
1221 due_on = new Date(due_on);
1231 if (now > due_on) {
1241 due_on = now;
125 }
126 } else {
1270 due_on = now;
128 }
1291 a = created_at - start;
1301 b = due_on - start;
1311 return [
132 {
133 'date': created_at,
134 'points': fn(a)
135 }, {
136 'date': due_on,
137 'points': fn(b)
138 }
139 ];
140 }
141 };
142
143}).call(this);
144

/home/radek/dev/burnchart/src/modules/github/issues.coffee

98%
51
50
1
LineHitsSource
11(function() {
21 var async, calcSize, config, oneStatus, request, _;
3
41 _ = require('lodash');
5
61 async = require('async');
7
81 config = require('../../models/config.coffee');
9
101 request = require('./request.coffee');
11
121 module.exports = {
13 fetchAll: function(repo, cb) {
1414 return async.parallel([_.partial(oneStatus, repo, 'open'), _.partial(oneStatus, repo, 'closed')], function(err, _arg) {
1514 var closed, open;
1614 open = _arg[0], closed = _arg[1];
1714 return cb(err, {
18 open: open,
19 closed: closed
20 });
21 });
22 }
23 };
24
251 calcSize = function(list) {
2626 var issue, size, _i, _len;
2726 switch (config.data.chart.points) {
28 case 'ONE_SIZE':
2916 size = list.length;
3016 for (_i = 0, _len = list.length; _i < _len; _i++) {
311006 issue = list[_i];
321006 issue.size = 1;
33 }
3416 break;
35 case 'LABELS':
3610 size = 0;
3710 list = _.filter(list, function(issue) {
3816 var labels;
3916 if (!(labels = issue.labels)) {
402 return false;
41 }
4214 issue.size = _.reduce(labels, function(sum, label) {
4316 var matches;
4416 if (!(matches = label.name.match(config.data.chart.size_label))) {
456 return sum;
46 }
4710 return sum += parseInt(matches[1]);
48 }, 0);
4914 size += issue.size;
5014 return !!issue.size;
51 });
5210 break;
53 default:
540 throw 500;
55 }
5626 return {
57 list: list,
58 size: size
59 };
60 };
61
621 oneStatus = function(repo, state, cb) {
6328 var done, fetchPage, results;
6428 results = [];
6528 done = function(err) {
6628 if (err) {
672 return cb(err);
68 }
6926 return cb(null, calcSize(results));
70 };
7128 return (fetchPage = function(page) {
7236 return request.allIssues(repo, {
73 state: state,
74 page: page
75 }, function(err, data) {
7636 if (err) {
772 return done(err);
78 }
7934 if (!data.length) {
806 return done(null, results);
81 }
8228 results = results.concat(_.sortBy(data, 'closed_at'));
8328 if (data.length < 100) {
8420 return done(null, results);
85 }
868 return fetchPage(page + 1);
87 });
88 })(1);
89 };
90
91}).call(this);
92

/home/radek/dev/burnchart/src/modules/github/request.coffee

72%
124
90
34
LineHitsSource
12(function() {
22 var config, defaults, error, headers, isReady, isValid, ready, request, response, stack, superagent, user, _;
3
42 _ = require('lodash');
5
62 superagent = require('superagent');
7
82 require('../../utils/mixins.coffee');
9
102 config = require('../../models/config.coffee');
11
122 user = require('../../models/user.coffee');
13
142 superagent.parse = {
15 'application/json': function(res) {
160 var e;
170 try {
180 return JSON.parse(res);
19 } catch (_error) {
200 e = _error;
210 return {};
22 }
23 }
24 };
25
262 defaults = {
27 'github': {
28 'host': 'api.github.com',
29 'protocol': 'https'
30 }
31 };
32
332 module.exports = {
34 repo: function(_arg, cb) {
350 var name, owner;
360 owner = _arg.owner, name = _arg.name;
370 if (!isValid({
38 owner: owner,
39 name: name
40 })) {
410 return cb('Request is malformed');
42 }
430 return ready(function() {
440 var data;
450 data = _.defaults({
46 'path': "/repos/" + owner + "/" + name,
47 'headers': headers(user.data.accessToken)
48 }, defaults.github);
490 return request(data, cb);
50 });
51 },
52 allMilestones: function(_arg, cb) {
531 var name, owner;
541 owner = _arg.owner, name = _arg.name;
551 if (!isValid({
56 owner: owner,
57 name: name
58 })) {
590 return cb('Request is malformed');
60 }
611 return ready(function() {
621 var data;
631 data = _.defaults({
64 'path': "/repos/" + owner + "/" + name + "/milestones",
65 'query': {
66 'state': 'open',
67 'sort': 'due_date',
68 'direction': 'asc'
69 },
70 'headers': headers(user.data.accessToken)
71 }, defaults.github);
721 return request(data, cb);
73 });
74 },
75 oneMilestone: function(_arg, cb) {
763 var milestone, name, owner;
773 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
783 if (!isValid({
79 owner: owner,
80 name: name,
81 milestone: milestone
82 })) {
830 return cb('Request is malformed');
84 }
853 return ready(function() {
863 var data;
873 data = _.defaults({
88 'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
89 'query': {
90 'state': 'open',
91 'sort': 'due_date',
92 'direction': 'asc'
93 },
94 'headers': headers(user.data.accessToken)
95 }, defaults.github);
963 return request(data, cb);
97 });
98 },
99 allIssues: function(_arg, query, cb) {
1002 var milestone, name, owner;
1012 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
1022 if (!isValid({
103 owner: owner,
104 name: name,
105 milestone: milestone
106 })) {
1070 return cb('Request is malformed');
108 }
1092 return ready(function() {
1102 var data;
1112 data = _.defaults({
112 'path': "/repos/" + owner + "/" + name + "/issues",
113 'query': _.extend(query, {
114 milestone: milestone,
115 'per_page': '100'
116 }),
117 'headers': headers(user.data.accessToken)
118 }, defaults.github);
1192 return request(data, cb);
120 });
121 }
122 };
123
1242 request = function(_arg, cb) {
1256 var exited, headers, host, k, path, protocol, q, query, req, timeout, v;
1266 protocol = _arg.protocol, host = _arg.host, path = _arg.path, query = _arg.query, headers = _arg.headers;
1276 exited = false;
1286 q = query ? '?' + ((function() {
1296 var _results;
1306 _results = [];
1316 for (k in query) {
13216 v = query[k];
13316 _results.push("" + k + "=" + v);
134 }
1356 return _results;
136 })()).join('&') : '';
1376 req = superagent.get("" + protocol + "://" + host + path + q);
1386 for (k in headers) {
13912 v = headers[k];
14012 req.set(k, v);
141 }
1426 timeout = setTimeout(function() {
1431 exited = true;
1441 return cb('Request has timed out');
145 }, config.data.request.timeout);
1466 return req.end(function(err, data) {
1475 if (exited) {
1480 return;
149 }
1505 exited = true;
1515 clearTimeout(timeout);
1525 return response(err, data, cb);
153 });
154 };
155
1562 response = function(err, data, cb) {
1575 var _ref;
1585 if (err) {
1590 return cb(error(err));
160 }
1615 if (data.statusType !== 2) {
1622 if ((data != null ? (_ref = data.body) != null ? _ref.message : void 0 : void 0) != null) {
1631 return cb(data.body.message);
164 }
1651 return cb(data.error.message);
166 }
1673 return cb(null, data.body);
168 };
169
1702 headers = function(token) {
1716 var h;
1726 h = {
173 'Content-Type': 'application/json',
174 'Accept': 'application/vnd.github.v3'
175 };
1766 if (token != null) {
1770 h.Authorization = "token " + token;
178 }
1796 return h;
180 };
181
1822 isValid = function(obj) {
1836 var key, rules, val;
1846 rules = {
185 'owner': function(val) {
1866 return val != null;
187 },
188 'name': function(val) {
1896 return val != null;
190 },
191 'milestone': function(val) {
1925 return _.isInt(val);
193 }
194 };
1956 for (key in obj) {
19617 val = obj[key];
19717 if (key in rules && !rules[key](val)) {
1980 return false;
199 }
200 }
2016 return true;
202 };
203
2042 isReady = user.data.ready;
205
2062 stack = [];
207
2082 ready = function(cb) {
2096 if (isReady) {
2106 return cb();
211 } else {
2120 return stack.push(cb);
213 }
214 };
215
2162 user.observe('ready', function(val) {
2174 var _results;
2184 isReady = val;
2194 if (val) {
2202 _results = [];
2212 while (stack.length) {
2220 _results.push(stack.shift()());
223 }
2242 return _results;
225 }
226 });
227
2282 error = function(err) {
2290 var message;
2300 switch (false) {
231 case !_.isString(err):
2320 message = err;
2330 break;
234 case !_.isArray(err):
2350 message = err[1];
2360 break;
237 case !(_.isObject(err) && _.isString(err.message)):
2380 message = err.message;
239 }
2400 if (!message) {
2410 try {
2420 message = JSON.stringify(err);
243 } catch (_error) {
2440 message = err.toString();
245 }
246 }
2470 return message;
248 };
249
250}).call(this);
251

/home/radek/dev/burnchart/src/modules/mediator.coffee

100%
5
5
0
LineHitsSource
11(function() {
21 var Mediator, Ractive;
3
41 Ractive = require('ractive');
5
61 Mediator = Ractive.extend({});
7
81 module.exports = new Mediator();
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/stats.coffee

97%
34
33
1
LineHitsSource
11(function() {
21 var moment, progress;
3
41 moment = require('moment');
5
61 progress = function(a, b) {
76 if (a + b === 0) {
80 return 0;
9 } else {
106 return 100 * (a / (b + a));
11 }
12 };
13
141 module.exports = function(milestone) {
1519 var a, b, c, days, isDone, isEmpty, isOnTime, isOverdue, points, time;
1619 if (milestone.stats != null) {
1714 return milestone.stats;
18 }
195 isDone = false;
205 isOnTime = true;
215 isOverdue = false;
225 isEmpty = true;
235 points = 0;
245 a = milestone.issues.closed.size;
255 b = milestone.issues.open.size;
265 if (a + b > 0) {
273 isEmpty = false;
283 points = progress(a, b);
293 if (points === 100) {
301 isDone = true;
31 }
32 }
335 if (milestone.due_on == null) {
342 return {
35 isOverdue: isOverdue,
36 isOnTime: isOnTime,
37 isDone: isDone,
38 isEmpty: isEmpty,
39 'progress': {
40 points: points
41 }
42 };
43 }
443 a = +new Date(milestone.created_at);
453 b = +(new Date);
463 c = +new Date(milestone.due_on);
473 if (b > c) {
481 isOverdue = true;
49 }
503 time = progress(b - a, c - b);
513 days = (moment(b).diff(moment(a), 'days')) / 100;
523 isOnTime = points > time;
533 return {
54 isDone: isDone,
55 days: days,
56 isOnTime: isOnTime,
57 isOverdue: isOverdue,
58 'progress': {
59 points: points,
60 time: time
61 }
62 };
63 };
64
65}).call(this);
66

/home/radek/dev/burnchart/src/utils/mixins.coffee

92%
13
12
1
LineHitsSource
11(function() {
21 var _;
3
41 _ = require('lodash');
5
61 _.mixin({
7 'pluckMany': function(source, keys) {
826 if (!_.isArray(keys)) {
90 throw '`keys` needs to be an Array';
10 }
1126 return _.map(source, function(item) {
1220 var obj;
1320 obj = {};
1420 _.each(keys, function(key) {
1540 return obj[key] = item[key];
16 });
1720 return obj;
18 });
19 },
20 'isInt': function(val) {
215 return !isNaN(val) && parseInt(Number(val)) === val && !isNaN(parseInt(val, 10));
22 }
23 });
24
25}).call(this);
26

/home/radek/dev/burnchart/src/utils/ractive/eventful.coffee

45%
24
11
13
LineHitsSource
11(function() {
21 var Ractive, mediator, _;
3
41 _ = require('lodash');
5
61 Ractive = require('ractive');
7
81 mediator = require('../../modules/mediator.coffee');
9
101 module.exports = Ractive.extend({
11 subscribe: function(name, cb, ctx) {
122 if (ctx == null) {
130 ctx = this;
14 }
152 if (!_.isArray(this._subs)) {
161 this._subs = [];
17 }
182 if (_.isFunction(cb)) {
192 return this._subs.push(mediator.on(name, _.bind(cb, ctx)));
20 } else {
210 return console.log("Warning: `cb` is not a function");
22 }
23 },
24 publish: function() {
250 return mediator.fire.apply(mediator, arguments);
26 },
27 onteardown: function() {
280 var sub, _i, _len, _ref, _results;
290 if (_.isArray(this._subs)) {
300 _ref = this._subs;
310 _results = [];
320 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
330 sub = _ref[_i];
340 if (_.isFunction(sub.cancel)) {
350 _results.push(sub.cancel());
36 } else {
370 _results.push(console.log("Warning: `sub.cancel` is not a function"));
38 }
39 }
400 return _results;
41 }
42 }
43 });
44
45}).call(this);
46

/home/radek/dev/burnchart/src/utils/ractive/model.coffee

100%
9
9
0
LineHitsSource
11(function() {
21 var Eventful;
3
41 Eventful = require('./eventful.coffee');
5
61 module.exports = function(opts) {
73 var Model, model;
83 Model = Eventful.extend(opts);
93 model = new Model();
103 model.render();
113 return model;
12 };
13
14}).call(this);
15
\ No newline at end of file +

Coverage

83%
478
399
79

/home/radek/dev/burnchart/src/models/config.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/config',
8 "data": {
9 "firebase": "burnchart",
10 "provider": "github",
11 "fields": {
12 "milestone": ["closed_issues", "created_at", "description", "due_on", "number", "open_issues", "title", "updated_at"]
13 },
14 "chart": {
15 "off_days": [],
16 "size_label": /^size (\d+)$/,
17 "points": 'ONE_SIZE'
18 },
19 "request": {
20 "timeout": 5e3
21 }
22 }
23 });
24
25}).call(this);
26

/home/radek/dev/burnchart/src/models/projects.coffee

80%
122
98
24
LineHitsSource
11(function() {
21 var Model, config, lscache, semver, sortedIndex, stats, user, _,
3 __slice = [].slice;
4
51 _ = require('lodash');
6
71 lscache = require('lscache');
8
91 sortedIndex = require('sortedindex-compare');
10
111 semver = require('semver');
12
131 Model = require('../utils/ractive/model.coffee');
14
151 config = require('../models/config.coffee');
16
171 stats = require('../modules/stats.coffee');
18
191 user = require('./user.coffee');
20
211 module.exports = new Model({
22 'name': 'models/projects',
23 'data': {
24 'sortBy': 'priority',
25 'sortFns': ['progress', 'priority', 'name']
26 },
27 comparator: function() {
2814 var deIdx, defaults, list, sortBy, _ref;
2914 _ref = this.data, list = _ref.list, sortBy = _ref.sortBy;
3014 deIdx = (function(_this) {
3114 return function(fn) {
3214 return function() {
3311 var i, j, rest, _arg;
3411 _arg = arguments[0], rest = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
3511 i = _arg[0], j = _arg[1];
3611 return fn.apply(_this, [[list[i], list[i].milestones[j]]].concat(rest));
37 };
38 };
39 })(this);
4014 defaults = function(arr, hash) {
417 var i, item, k, keys, p, ref, v, _i, _len, _results;
427 _results = [];
437 for (_i = 0, _len = arr.length; _i < _len; _i++) {
4414 item = arr[_i];
4514 _results.push((function() {
4614 var _results1;
4714 _results1 = [];
4814 for (k in hash) {
4926 v = hash[k];
5026 ref = item;
5126 _results1.push((function() {
5226 var _j, _len1, _ref1, _results2;
5326 _ref1 = keys = k.split('.');
5426 _results2 = [];
5526 for (i = _j = 0, _len1 = _ref1.length; _j < _len1; i = ++_j) {
5666 p = _ref1[i];
5766 if (i === keys.length - 1) {
5826 _results2.push(ref[p] != null ? ref[p] : ref[p] = v);
59 } else {
6040 _results2.push(ref = ref[p] != null ? ref[p] : ref[p] = {});
61 }
62 }
6326 return _results2;
64 })());
65 }
6614 return _results1;
67 })());
68 }
697 return _results;
70 };
7114 switch (sortBy) {
72 case 'progress':
732 return deIdx(function(_arg, _arg1) {
741 var aM, aP, bM, bP;
751 aP = _arg[0], aM = _arg[1];
761 bP = _arg1[0], bM = _arg1[1];
771 defaults([aM, bM], {
78 'stats.progress.points': 0
79 });
801 return aM.stats.progress.points - bM.stats.progress.points;
81 });
82 case 'priority':
837 return deIdx(function(_arg, _arg1) {
846 var $a, $b, aM, aP, bM, bP, _ref1;
856 aP = _arg[0], aM = _arg[1];
866 bP = _arg1[0], bM = _arg1[1];
876 defaults([aM, bM], {
88 'stats.progress.time': 0,
89 'stats.days': 1e3
90 });
916 _ref1 = _.map([aM, bM], function(_arg2) {
9212 var stats;
9312 stats = _arg2.stats;
9412 return (stats.progress.points - stats.progress.time) * stats.days;
95 }), $a = _ref1[0], $b = _ref1[1];
966 return $b - $a;
97 });
98 case 'name':
995 return deIdx(function(_arg, _arg1) {
1004 var aM, aP, bM, bP, name, owner;
1014 aP = _arg[0], aM = _arg[1];
1024 bP = _arg1[0], bM = _arg1[1];
1034 if (owner = bP.owner.localeCompare(aP.owner)) {
1040 return owner;
105 }
1064 if (name = bP.name.localeCompare(aP.name)) {
1070 return name;
108 }
1094 if (semver.valid(bM.title) && semver.valid(aM.title)) {
1101 return semver.gt(bM.title, aM.title);
111 } else {
1123 return bM.title.localeCompare(aM.title);
113 }
114 });
115 default:
1160 return function() {
1170 return 0;
118 };
119 }
120 },
121 find: function(project) {
1220 return _.find(this.data.list, project);
123 },
124 exists: function() {
1250 return !!this.find.apply(this, arguments);
126 },
127 add: function(project) {
1280 if (!this.exists(project)) {
1290 return this.push('list', project);
130 }
131 },
132 findIndex: function(_arg) {
13314 var name, owner;
13414 owner = _arg.owner, name = _arg.name;
13514 return _.findIndex(this.data.list, {
136 owner: owner,
137 name: name
138 });
139 },
140 addMilestone: function(project, milestone) {
14114 var i, j;
14214 _.extend(milestone, {
143 'stats': stats(milestone)
144 });
14514 if ((i = this.findIndex(project)) < 0) {
1460 throw 500;
147 }
14814 if (project.milestones != null) {
1498 this.push("list." + i + ".milestones", milestone);
1508 j = this.data.list[i].milestones.length - 1;
151 } else {
1526 this.set("list." + i + ".milestones", [milestone]);
1536 j = 0;
154 }
15514 return this.sort([i, j], [project, milestone]);
156 },
157 saveError: function(project, err) {
1580 var idx;
1590 if ((idx = this.findIndex(project)) > -1) {
1600 if (project.errors != null) {
1610 return this.push("list." + idx + ".errors", err);
162 } else {
1630 return this.set("list." + idx + ".errors", [err]);
164 }
165 } else {
1660 throw 500;
167 }
168 },
169 demo: function() {
1700 return this.set({
171 'list': [
172 {
173 'owner': 'mbostock',
174 'name': 'd3'
175 }, {
176 'owner': 'medic',
177 'name': 'medic-webapp'
178 }, {
179 'owner': 'ractivejs',
180 'name': 'ractive'
181 }, {
182 'owner': 'radekstepan',
183 'name': 'disposable'
184 }, {
185 'owner': 'rails',
186 'name': 'rails'
187 }, {
188 'owner': 'rails',
189 'name': 'spring'
190 }
191 ],
192 'index': []
193 });
194 },
195 clear: function() {
1966 return this.set({
197 'list': [],
198 'index': []
199 });
200 },
201 sort: function(ref, data) {
20217 var i, idx, index, j, m, p, _i, _j, _len, _len1, _ref, _ref1;
20317 index = this.data.index || [];
20417 if (ref) {
20514 idx = sortedIndex(index, data, this.comparator());
20614 index.splice(idx, 0, ref);
207 } else {
2083 _ref = this.data.list;
2093 for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
2100 p = _ref[i];
2110 if (p.milestones == null) {
2120 continue;
213 }
2140 _ref1 = p.milestones;
2150 for (j = _j = 0, _len1 = _ref1.length; _j < _len1; j = ++_j) {
2160 m = _ref1[j];
2170 idx = sortedIndex(index, [p, m], this.comparator());
2180 index.splice(idx, 0, [i, j]);
219 }
220 }
221 }
22217 return this.set('index', index);
223 },
224 onconstruct: function() {
2251 this.subscribe('!projects/add', this.add, this);
2261 return this.subscribe('!projects/demo', this.demo, this);
227 },
228 onrender: function() {
2291 this.set('list', lscache.get('projects') || []);
2301 this.observe('list', function(projects) {
23126 return lscache.set('projects', _.pluckMany(projects, ['owner', 'name']));
232 }, {
233 'init': false
234 });
2351 return this.observe('sortBy', function() {
2363 this.set('index', null);
2373 return this.sort();
238 }, {
239 'init': false
240 });
241 }
242 });
243
244}).call(this);
245

/home/radek/dev/burnchart/src/models/user.coffee

100%
4
4
0
LineHitsSource
11(function() {
21 var Model;
3
41 Model = require('../utils/ractive/model.coffee');
5
61 module.exports = new Model({
7 'name': 'models/user'
8 });
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/chart/lines.coffee

94%
88
83
5
LineHitsSource
11(function() {
21 var config, d3, moment, _,
30 __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
4
51 _ = require('lodash');
6
71 d3 = require('d3');
8
91 moment = require('moment');
10
111 config = require('../../models/config.coffee');
12
131 module.exports = {
14 actual: function(issues, created_at, total) {
151 var head, max, min, range, rest;
161 head = [
17 {
18 'date': moment(created_at).toJSON(),
19 'points': total
20 }
21 ];
221 min = +Infinity;
231 max = -Infinity;
241 rest = _.map(issues, function(issue) {
253 var closed_at, size;
263 size = issue.size, closed_at = issue.closed_at;
273 if (size < min) {
283 min = size;
29 }
303 if (size > max) {
311 max = size;
32 }
333 issue.date = moment(closed_at).toJSON();
343 issue.points = total -= size;
353 return issue;
36 });
371 range = d3.scale.linear().domain([min, max]).range([5, 8]);
381 rest = _.map(rest, function(issue) {
393 issue.radius = range(issue.size);
403 return issue;
41 });
421 return [].concat(head, rest);
43 },
44 ideal: function(a, b, total) {
451 var days, length, now, once, velocity, _ref;
461 if (b < a) {
470 _ref = [a, b], b = _ref[0], a = _ref[1];
48 }
491 a = moment(a);
501 b = b != null ? moment(b) : moment.utc();
511 days = [];
521 length = 0;
531 (once = function(inc) {
543 var day, day_of;
553 day = a.add(1, 'days');
563 if (!(day_of = day.weekday())) {
571 day_of = 7;
58 }
593 if (__indexOf.call(config.data.chart.off_days, day_of) >= 0) {
600 days.push({
61 'date': day.toJSON(),
62 'off_day': true
63 });
64 } else {
653 length += 1;
663 days.push({
67 'date': day.toJSON()
68 });
69 }
703 if (!(day > b)) {
712 return once(inc + 1);
72 }
73 })(0);
741 velocity = total / (length - 1);
751 days = _.map(days, function(day, i) {
763 day.points = total;
773 if (days[i] && !days[i].off_day) {
783 total -= velocity;
79 }
803 return day;
81 });
821 if ((now = moment.utc()) > b) {
831 days.push({
84 'date': now.toJSON(),
85 'points': 0
86 });
87 }
881 return days;
89 },
90 trend: function(actual, created_at, due_on) {
911 var a, b, b1, c1, e, first, fn, intercept, l, last, now, slope, start, values;
921 if (!actual.length) {
930 return [];
94 }
951 first = actual[0], last = actual[actual.length - 1];
961 start = moment(first.date);
971 values = _.map(actual, function(_arg) {
983 var date, points;
993 date = _arg.date, points = _arg.points;
1003 return [moment(date).diff(start), points];
101 });
1021 now = moment.utc();
1031 values.push([now.diff(start), last.points]);
1041 b1 = 0;
1051 e = 0;
1061 c1 = 0;
1071 a = (l = values.length) * _.reduce(values, function(sum, _arg) {
1084 var a, b;
1094 a = _arg[0], b = _arg[1];
1104 b1 += a;
1114 e += b;
1124 c1 += Math.pow(a, 2);
1134 return sum + (a * b);
114 }, 0);
1151 slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
1161 intercept = (e - (slope * b1)) / l;
1171 fn = function(x) {
1182 return slope * x + intercept;
119 };
1201 created_at = moment(created_at);
1211 if (due_on) {
1221 due_on = moment(due_on);
1231 if (now > due_on) {
1241 due_on = now;
125 }
126 } else {
1270 due_on = now;
128 }
1291 a = created_at.diff(start);
1301 b = due_on.diff(start);
1311 return [
132 {
133 'date': created_at.toJSON(),
134 'points': fn(a)
135 }, {
136 'date': due_on.toJSON(),
137 'points': fn(b)
138 }
139 ];
140 }
141 };
142
143}).call(this);
144

/home/radek/dev/burnchart/src/modules/github/issues.coffee

98%
51
50
1
LineHitsSource
11(function() {
21 var async, calcSize, config, oneStatus, request, _;
3
41 _ = require('lodash');
5
61 async = require('async');
7
81 config = require('../../models/config.coffee');
9
101 request = require('./request.coffee');
11
121 module.exports = {
13 fetchAll: function(repo, cb) {
1414 return async.parallel([_.partial(oneStatus, repo, 'open'), _.partial(oneStatus, repo, 'closed')], function(err, _arg) {
1514 var closed, open;
1614 open = _arg[0], closed = _arg[1];
1714 return cb(err, {
18 open: open,
19 closed: closed
20 });
21 });
22 }
23 };
24
251 calcSize = function(list) {
2626 var issue, size, _i, _len;
2726 switch (config.data.chart.points) {
28 case 'ONE_SIZE':
2916 size = list.length;
3016 for (_i = 0, _len = list.length; _i < _len; _i++) {
311006 issue = list[_i];
321006 issue.size = 1;
33 }
3416 break;
35 case 'LABELS':
3610 size = 0;
3710 list = _.filter(list, function(issue) {
3816 var labels;
3916 if (!(labels = issue.labels)) {
402 return false;
41 }
4214 issue.size = _.reduce(labels, function(sum, label) {
4316 var matches;
4416 if (!(matches = label.name.match(config.data.chart.size_label))) {
456 return sum;
46 }
4710 return sum += parseInt(matches[1]);
48 }, 0);
4914 size += issue.size;
5014 return !!issue.size;
51 });
5210 break;
53 default:
540 throw 500;
55 }
5626 return {
57 list: list,
58 size: size
59 };
60 };
61
621 oneStatus = function(repo, state, cb) {
6328 var done, fetchPage, results;
6428 results = [];
6528 done = function(err) {
6628 if (err) {
672 return cb(err);
68 }
6926 return cb(null, calcSize(results));
70 };
7128 return (fetchPage = function(page) {
7236 return request.allIssues(repo, {
73 state: state,
74 page: page
75 }, function(err, data) {
7636 if (err) {
772 return done(err);
78 }
7934 if (!data.length) {
806 return done(null, results);
81 }
8228 results = results.concat(_.sortBy(data, 'closed_at'));
8328 if (data.length < 100) {
8420 return done(null, results);
85 }
868 return fetchPage(page + 1);
87 });
88 })(1);
89 };
90
91}).call(this);
92

/home/radek/dev/burnchart/src/modules/github/request.coffee

72%
124
90
34
LineHitsSource
12(function() {
22 var config, defaults, error, headers, isReady, isValid, ready, request, response, stack, superagent, user, _;
3
42 _ = require('lodash');
5
62 superagent = require('superagent');
7
82 require('../../utils/mixins.coffee');
9
102 config = require('../../models/config.coffee');
11
122 user = require('../../models/user.coffee');
13
142 superagent.parse = {
15 'application/json': function(res) {
160 var e;
170 try {
180 return JSON.parse(res);
19 } catch (_error) {
200 e = _error;
210 return {};
22 }
23 }
24 };
25
262 defaults = {
27 'github': {
28 'host': 'api.github.com',
29 'protocol': 'https'
30 }
31 };
32
332 module.exports = {
34 repo: function(_arg, cb) {
350 var name, owner;
360 owner = _arg.owner, name = _arg.name;
370 if (!isValid({
38 owner: owner,
39 name: name
40 })) {
410 return cb('Request is malformed');
42 }
430 return ready(function() {
440 var data;
450 data = _.defaults({
46 'path': "/repos/" + owner + "/" + name,
47 'headers': headers(user.data.accessToken)
48 }, defaults.github);
490 return request(data, cb);
50 });
51 },
52 allMilestones: function(_arg, cb) {
531 var name, owner;
541 owner = _arg.owner, name = _arg.name;
551 if (!isValid({
56 owner: owner,
57 name: name
58 })) {
590 return cb('Request is malformed');
60 }
611 return ready(function() {
621 var data;
631 data = _.defaults({
64 'path': "/repos/" + owner + "/" + name + "/milestones",
65 'query': {
66 'state': 'open',
67 'sort': 'due_date',
68 'direction': 'asc'
69 },
70 'headers': headers(user.data.accessToken)
71 }, defaults.github);
721 return request(data, cb);
73 });
74 },
75 oneMilestone: function(_arg, cb) {
763 var milestone, name, owner;
773 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
783 if (!isValid({
79 owner: owner,
80 name: name,
81 milestone: milestone
82 })) {
830 return cb('Request is malformed');
84 }
853 return ready(function() {
863 var data;
873 data = _.defaults({
88 'path': "/repos/" + owner + "/" + name + "/milestones/" + milestone,
89 'query': {
90 'state': 'open',
91 'sort': 'due_date',
92 'direction': 'asc'
93 },
94 'headers': headers(user.data.accessToken)
95 }, defaults.github);
963 return request(data, cb);
97 });
98 },
99 allIssues: function(_arg, query, cb) {
1002 var milestone, name, owner;
1012 owner = _arg.owner, name = _arg.name, milestone = _arg.milestone;
1022 if (!isValid({
103 owner: owner,
104 name: name,
105 milestone: milestone
106 })) {
1070 return cb('Request is malformed');
108 }
1092 return ready(function() {
1102 var data;
1112 data = _.defaults({
112 'path': "/repos/" + owner + "/" + name + "/issues",
113 'query': _.extend(query, {
114 milestone: milestone,
115 'per_page': '100'
116 }),
117 'headers': headers(user.data.accessToken)
118 }, defaults.github);
1192 return request(data, cb);
120 });
121 }
122 };
123
1242 request = function(_arg, cb) {
1256 var exited, headers, host, k, path, protocol, q, query, req, timeout, v;
1266 protocol = _arg.protocol, host = _arg.host, path = _arg.path, query = _arg.query, headers = _arg.headers;
1276 exited = false;
1286 q = query ? '?' + ((function() {
1296 var _results;
1306 _results = [];
1316 for (k in query) {
13216 v = query[k];
13316 _results.push("" + k + "=" + v);
134 }
1356 return _results;
136 })()).join('&') : '';
1376 req = superagent.get("" + protocol + "://" + host + path + q);
1386 for (k in headers) {
13912 v = headers[k];
14012 req.set(k, v);
141 }
1426 timeout = setTimeout(function() {
1431 exited = true;
1441 return cb('Request has timed out');
145 }, config.data.request.timeout);
1466 return req.end(function(err, data) {
1475 if (exited) {
1480 return;
149 }
1505 exited = true;
1515 clearTimeout(timeout);
1525 return response(err, data, cb);
153 });
154 };
155
1562 response = function(err, data, cb) {
1575 var _ref;
1585 if (err) {
1590 return cb(error(err));
160 }
1615 if (data.statusType !== 2) {
1622 if ((data != null ? (_ref = data.body) != null ? _ref.message : void 0 : void 0) != null) {
1631 return cb(data.body.message);
164 }
1651 return cb(data.error.message);
166 }
1673 return cb(null, data.body);
168 };
169
1702 headers = function(token) {
1716 var h;
1726 h = {
173 'Content-Type': 'application/json',
174 'Accept': 'application/vnd.github.v3'
175 };
1766 if (token != null) {
1770 h.Authorization = "token " + token;
178 }
1796 return h;
180 };
181
1822 isValid = function(obj) {
1836 var key, rules, val;
1846 rules = {
185 'owner': function(val) {
1866 return val != null;
187 },
188 'name': function(val) {
1896 return val != null;
190 },
191 'milestone': function(val) {
1925 return _.isInt(val);
193 }
194 };
1956 for (key in obj) {
19617 val = obj[key];
19717 if (key in rules && !rules[key](val)) {
1980 return false;
199 }
200 }
2016 return true;
202 };
203
2042 isReady = user.data.ready;
205
2062 stack = [];
207
2082 ready = function(cb) {
2096 if (isReady) {
2106 return cb();
211 } else {
2120 return stack.push(cb);
213 }
214 };
215
2162 user.observe('ready', function(val) {
2174 var _results;
2184 isReady = val;
2194 if (val) {
2202 _results = [];
2212 while (stack.length) {
2220 _results.push(stack.shift()());
223 }
2242 return _results;
225 }
226 });
227
2282 error = function(err) {
2290 var message;
2300 switch (false) {
231 case !_.isString(err):
2320 message = err;
2330 break;
234 case !_.isArray(err):
2350 message = err[1];
2360 break;
237 case !(_.isObject(err) && _.isString(err.message)):
2380 message = err.message;
239 }
2400 if (!message) {
2410 try {
2420 message = JSON.stringify(err);
243 } catch (_error) {
2440 message = err.toString();
245 }
246 }
2470 return message;
248 };
249
250}).call(this);
251

/home/radek/dev/burnchart/src/modules/mediator.coffee

100%
5
5
0
LineHitsSource
11(function() {
21 var Mediator, Ractive;
3
41 Ractive = require('ractive');
5
61 Mediator = Ractive.extend({});
7
81 module.exports = new Mediator();
9
10}).call(this);
11

/home/radek/dev/burnchart/src/modules/stats.coffee

97%
34
33
1
LineHitsSource
11(function() {
21 var moment, progress;
3
41 moment = require('moment');
5
61 progress = function(a, b) {
76 if (a + b === 0) {
80 return 0;
9 } else {
106 return 100 * (a / (b + a));
11 }
12 };
13
141 module.exports = function(milestone) {
1519 var a, b, c, days, isDone, isEmpty, isOnTime, isOverdue, points, time;
1619 if (milestone.stats != null) {
1714 return milestone.stats;
18 }
195 isDone = false;
205 isOnTime = true;
215 isOverdue = false;
225 isEmpty = true;
235 points = 0;
245 a = milestone.issues.closed.size;
255 b = milestone.issues.open.size;
265 if (a + b > 0) {
273 isEmpty = false;
283 points = progress(a, b);
293 if (points === 100) {
301 isDone = true;
31 }
32 }
335 if (milestone.due_on == null) {
342 return {
35 isOverdue: isOverdue,
36 isOnTime: isOnTime,
37 isDone: isDone,
38 isEmpty: isEmpty,
39 'progress': {
40 points: points
41 }
42 };
43 }
443 a = +new Date(milestone.created_at);
453 b = +(new Date);
463 c = +new Date(milestone.due_on);
473 if (b > c) {
481 isOverdue = true;
49 }
503 time = progress(b - a, c - b);
513 days = (moment(b).diff(moment(a), 'days')) / 100;
523 isOnTime = points > time;
533 return {
54 isDone: isDone,
55 days: days,
56 isOnTime: isOnTime,
57 isOverdue: isOverdue,
58 'progress': {
59 points: points,
60 time: time
61 }
62 };
63 };
64
65}).call(this);
66

/home/radek/dev/burnchart/src/utils/mixins.coffee

92%
13
12
1
LineHitsSource
11(function() {
21 var _;
3
41 _ = require('lodash');
5
61 _.mixin({
7 'pluckMany': function(source, keys) {
826 if (!_.isArray(keys)) {
90 throw '`keys` needs to be an Array';
10 }
1126 return _.map(source, function(item) {
1220 var obj;
1320 obj = {};
1420 _.each(keys, function(key) {
1540 return obj[key] = item[key];
16 });
1720 return obj;
18 });
19 },
20 'isInt': function(val) {
215 return !isNaN(val) && parseInt(Number(val)) === val && !isNaN(parseInt(val, 10));
22 }
23 });
24
25}).call(this);
26

/home/radek/dev/burnchart/src/utils/ractive/eventful.coffee

45%
24
11
13
LineHitsSource
11(function() {
21 var Ractive, mediator, _;
3
41 _ = require('lodash');
5
61 Ractive = require('ractive');
7
81 mediator = require('../../modules/mediator.coffee');
9
101 module.exports = Ractive.extend({
11 subscribe: function(name, cb, ctx) {
122 if (ctx == null) {
130 ctx = this;
14 }
152 if (!_.isArray(this._subs)) {
161 this._subs = [];
17 }
182 if (_.isFunction(cb)) {
192 return this._subs.push(mediator.on(name, _.bind(cb, ctx)));
20 } else {
210 return console.log("Warning: `cb` is not a function");
22 }
23 },
24 publish: function() {
250 return mediator.fire.apply(mediator, arguments);
26 },
27 onteardown: function() {
280 var sub, _i, _len, _ref, _results;
290 if (_.isArray(this._subs)) {
300 _ref = this._subs;
310 _results = [];
320 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
330 sub = _ref[_i];
340 if (_.isFunction(sub.cancel)) {
350 _results.push(sub.cancel());
36 } else {
370 _results.push(console.log("Warning: `sub.cancel` is not a function"));
38 }
39 }
400 return _results;
41 }
42 }
43 });
44
45}).call(this);
46

/home/radek/dev/burnchart/src/utils/ractive/model.coffee

100%
9
9
0
LineHitsSource
11(function() {
21 var Eventful;
3
41 Eventful = require('./eventful.coffee');
5
61 module.exports = function(opts) {
73 var Model, model;
83 Model = Eventful.extend(opts);
93 model = new Model();
103 model.render();
113 return model;
12 };
13
14}).call(this);
15
\ No newline at end of file diff --git a/docs/TODO.md b/docs/TODO.md index 2720597..83d4a37 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -8,7 +8,7 @@ - [ ] 4 until GH fix milestone start date then provide an option to specify it; for example a text like this: `starts: 09-10-2014` in the description which we provide regex for in the config - [ ] 5 be able to config options through ui that currently have to be hardcoded in the config - [ ] 5 be able to delete added projects; on the project page listing all milestone, enable the cog at the bottom of the table, clicking it slides a link with a dustbin next to it which deletes the project -- [ ] 3 dates not parsing well with timezones, see ideal lines test file +- [ ] 3 check that we are using moment and toJSON all the way until chart view; tests checking res from github in milestones and issues ###Normal @@ -22,7 +22,7 @@ - [ ] 3 In add a project form autocomplete on my username, orgs I am member of and repos I have access to, use code from [elastic-med](https://github.com/intermine/intermine-apps-c/blob/master/elastic-med/src/components/search.coffee#L24-L46) to show the first option with Tab doing the autocomplete - [ ] 4 show number of tasks, points, days left, progress bar in the header of a chart page, just like in Assembly - [ ] 3 be able to specify milestone by name (will nicely show in title), so when we type in `owner/name/name` it should resolve the number -- [ ] 3 trendline is sometimes cutting into axes, but maybe it was just an interpolation +- [ ] 3 trendline is sometimes cutting into axes, see `rails/rails/36` - [ ] 3 deal with Firebase timing out, are we still logged-in? Show a warning page telling the people to refresh the browser (adding a button to do the same) - [ ] 3 use issue title to determine size - [ ] 3 the app bundle (albeit uncompressed) clocks in at 1.5MB, reduce the size (`d3` is huge (use [grunt-smash](https://github.com/cvisco/grunt-smash), [docs here](https://github.com/mbostock/smash/wiki))) diff --git a/src/models/config.coffee b/src/models/config.coffee index 87072d9..61093fa 100644 --- a/src/models/config.coffee +++ b/src/models/config.coffee @@ -25,8 +25,6 @@ module.exports = new Model "chart": # Days we are not working. Mon = 1 "off_days": [ ] - # How do we parse GitHub dates? - "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/ # How does a size label look like? "size_label": /^size (\d+)$/ # Process all issues as one size (ONE_SIZE) or use labels (LABELS). diff --git a/src/modules/chart/lines.coffee b/src/modules/chart/lines.coffee index 6c435b4..be42895 100644 --- a/src/modules/chart/lines.coffee +++ b/src/modules/chart/lines.coffee @@ -1,5 +1,6 @@ -_ = require 'lodash' -d3 = require 'd3' +_ = require 'lodash' +d3 = require 'd3' +moment = require 'moment' config = require '../../models/config.coffee' @@ -11,7 +12,7 @@ module.exports = # `total`: total number of points (open & closed issues) actual: (issues, created_at, total) -> head = [ { - 'date': new Date created_at + 'date': do moment(created_at).toJSON 'points': total } ] @@ -25,7 +26,7 @@ module.exports = max = size if size > max # Dropping points remaining. - issue.date = new Date closed_at + issue.date = do moment(closed_at).toJSON issue.points = total -= size issue @@ -43,30 +44,29 @@ module.exports = # `b`: milestone end date # `total`: total number of points (open & closed issues) ideal: (a, b, total) -> - # Swap? + # Swap if end is before the start... [ b, a ] = [ a, b ] if b < a - # We start here adding days to `d`. - [ y, m, d ] = _.map a.match(config.data.chart.datetime)[1].split('-'), (v) -> parseInt v - # We want to end here. - cutoff = new Date(b) + a = moment a + # Do we have a due date? + b = if b? then moment b else do moment.utc # Go through the beginning to the end skipping off days. days = [] ; length = 0 do once = (inc = 0) -> - # A new day. - day = new Date y, m - 1, d + inc + # A new day. TODO: deal with hours and minutes! + day = a.add 1, 'days' # Does this day count? - day_of = 7 if !day_of = day.getDay() + day_of = 7 unless day_of = do day.weekday if day_of in config.data.chart.off_days - days.push { date: day, off_day: yes } + days.push { 'date': do day.toJSON, 'off_day': yes } else length += 1 - days.push { date: day } + days.push { 'date': do day.toJSON } # Go again? - once(inc + 1) unless day > cutoff + once(inc + 1) unless day > b # Map points on the array of days now. velocity = total / (length - 1) @@ -77,7 +77,7 @@ module.exports = day # Do we need to make a link to right now? - days.push { date: now, points: 0 } if (now = new Date) > cutoff + days.push { 'date': do now.toJSON, 'points': 0 } if (now = do moment.utc) > b days @@ -85,15 +85,17 @@ module.exports = trend: (actual, created_at, due_on) -> return [] unless actual.length - start = +actual[0].date + [ first, ..., last ] = actual + + start = moment first.date # Values is a list of time from the start and points remaining. values = _.map actual, ({ date, points }) -> - [ +date - start, points ] + [ moment(date).diff(start), points ] # Now is an actual point too. - last = actual[actual.length - 1] - values.push [ + new Date - start, last.points ] + now = do moment.utc + values.push [ now.diff(start), last.points ] # http://classroom.synonym.com/calculate-trendline-2709.html b1 = 0 ; e = 0 ; c1 = 0 @@ -105,30 +107,30 @@ module.exports = slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2))) intercept = (e - (slope * b1)) / l + fn = (x) -> slope * x + intercept # Milestone always has a creation date. - created_at = new Date created_at + created_at = moment created_at - now = new Date # Due date specified. if due_on - due_on = new Date due_on + due_on = moment due_on # In the past? due_on = now if now > due_on # No due date else due_on = now - a = created_at - start - b = due_on - start + a = created_at.diff start + b = due_on.diff start [ { - 'date': created_at - 'points': fn(a) + 'date': do created_at.toJSON + 'points': fn a }, { - 'date': due_on - 'points': fn(b) + 'date': do due_on.toJSON + 'points': fn b } ] \ No newline at end of file diff --git a/src/views/chart.coffee b/src/views/chart.coffee index 802a94e..bf1f94f 100644 --- a/src/views/chart.coffee +++ b/src/views/chart.coffee @@ -48,12 +48,16 @@ module.exports = Ractive.extend # Line generator. line = d3.svg.line() .interpolate("linear") - .x( (d) -> x(d.date) ) + .x( (d) -> x(new Date(d.date)) ) # convert to Date only now .y( (d) -> y(d.points) ) # Get the minimum and maximum date, and initial points. - x.domain([ ideal[0].date, ideal[ideal.length - 1].date ]) - y.domain([ 0, ideal[0].points ]).nice() + [ first, ..., last ] = ideal + x.domain [ + new Date(first.date) + new Date(last.date) + ] + y.domain([ 0, first.points ]).nice() # Add an SVG element with the desired dimensions and margin. svg = d3.select(this.el.querySelector('#chart')).append("svg") @@ -93,9 +97,9 @@ module.exports = Ractive.extend # Add a line showing where we are now. svg.append("svg:line") .attr("class", "today") - .attr("x1", x(new Date())) + .attr("x1", x(new Date)) .attr("y1", 0) - .attr("x2", x(new Date())) + .attr("x2", x(new Date)) .attr("y2", height) # Add the ideal line path. @@ -131,7 +135,7 @@ module.exports = Ractive.extend .attr("xlink:href", ({ html_url }) -> html_url ) .attr("xlink:show", 'new') .append('svg:circle') - .attr("cx", ({ date }) -> x date ) + .attr("cx", ({ date }) -> x new Date date ) .attr("cy", ({ points }) -> y points ) .attr("r", ({ radius }) -> 5 ) # fixed for now .on('mouseover', tooltip.show) diff --git a/test/lines.coffee b/test/lines.coffee index 2fe3888..f42670a 100644 --- a/test/lines.coffee +++ b/test/lines.coffee @@ -18,17 +18,19 @@ module.exports = do done 'lines - ideal': (done) -> + # Dates are coming in without timezone information, so UTC. a = '2011-04-01T00:00:00Z' b = '2011-04-03T00:00:00Z' line = lines.ideal(a, b, 4)[ 0...3 ] assert.deepEqual line, [ - { 'date': new Date('2011-04-01T06:00:00Z'), 'points': 4 } - { 'date': new Date('2011-04-02T06:00:00Z'), 'points': 2 } - { 'date': new Date('2011-04-03T06:00:00Z'), 'points': 0 } + { 'date': '2011-04-02T00:00:00.000Z', 'points': 4 } + { 'date': '2011-04-03T00:00:00.000Z', 'points': 2 } + { 'date': '2011-04-04T00:00:00.000Z', 'points': 0 } ] + do done 'lines - trend': (done) ->