mirror of
https://github.com/status-im/burnchart.git
synced 2025-02-18 13:26:54 +00:00
chart skeleton and tests
This commit is contained in:
parent
eec0259fea
commit
56aedfeb23
@ -8,6 +8,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^1.5.2",
|
"async": "^1.5.2",
|
||||||
"classnames": "^2.2.3",
|
"classnames": "^2.2.3",
|
||||||
|
"d3": "^3.5.12",
|
||||||
|
"d3-tip": "^0.6.7",
|
||||||
"deep-diff": "^0.3.3",
|
"deep-diff": "^0.3.3",
|
||||||
"firebase": "^2.3.2",
|
"firebase": "^2.3.2",
|
||||||
"lesshat": "^3.0.2",
|
"lesshat": "^3.0.2",
|
||||||
|
@ -93,12 +93,18 @@ export default React.createClass({
|
|||||||
// Show project milestones.
|
// Show project milestones.
|
||||||
milestones(owner, name) {
|
milestones(owner, name) {
|
||||||
document.title = `${owner}/${name}`;
|
document.title = `${owner}/${name}`;
|
||||||
process.nextTick(() => { actions.emit('projects.load', { owner, name }); });
|
process.nextTick(() => {
|
||||||
|
actions.emit('projects.load', { owner, name });
|
||||||
|
});
|
||||||
return <MilestonesPage owner={owner} name={name} />;
|
return <MilestonesPage owner={owner} name={name} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show a project milestone chart.
|
// Show a project milestone chart.
|
||||||
chart(owner, name, milestone) {
|
chart(owner, name, milestone) {
|
||||||
|
document.title = `${owner}/${name}/${milestone}`;
|
||||||
|
process.nextTick(() => {
|
||||||
|
actions.emit('projects.load', { owner, name, milestone });
|
||||||
|
});
|
||||||
return <ChartPage owner={owner} name={name} milestone={milestone} />;
|
return <ChartPage owner={owner} name={name} milestone={milestone} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
38
src/js/components/Chart.jsx
Normal file
38
src/js/components/Chart.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import d3 from 'd3';
|
||||||
|
import d3Tip from 'd3-tip';
|
||||||
|
|
||||||
|
d3Tip(d3);
|
||||||
|
|
||||||
|
import Format from '../mixins/Format.js';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
|
||||||
|
displayName: 'Chart.jsx',
|
||||||
|
|
||||||
|
mixins: [ Format ],
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let milestone = this.props.milestone;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="title">
|
||||||
|
<div className="wrap">
|
||||||
|
<h2 className="title">{this._title(milestone.title)}</h2>
|
||||||
|
<span className="sub">{this._due(milestone.due_on)}</span>
|
||||||
|
<div className="description">{this._markdown(milestone.description)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="content" className="wrap">
|
||||||
|
<div id="chart" ref="el" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
console.log(this.refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
@ -22,7 +22,7 @@ export default {
|
|||||||
// Markdown formatting.
|
// Markdown formatting.
|
||||||
// TODO: works?
|
// TODO: works?
|
||||||
_markdown(...args) {
|
_markdown(...args) {
|
||||||
marked.apply(args);
|
marked.apply(null, args);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Format milestone title.
|
// Format milestone title.
|
||||||
|
@ -13,10 +13,5 @@ _.mixin({
|
|||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
isInt: (val) => {
|
|
||||||
return !isNaN(val) && parseInt(Number(val)) === val &&
|
|
||||||
!isNaN(parseInt(val, 10));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
24
src/js/modules/chart/axes.js
Normal file
24
src/js/modules/chart/axes.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import d3 from 'd3';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
horizontal(height, x) {
|
||||||
|
return d3.svg.axis().scale(x)
|
||||||
|
.orient("bottom")
|
||||||
|
// Show vertical lines...
|
||||||
|
.tickSize(-height)
|
||||||
|
// with day of the month...
|
||||||
|
.tickFormat((d) => { return d.getDate(); })
|
||||||
|
// and give us a spacer.
|
||||||
|
.tickPadding(10);
|
||||||
|
},
|
||||||
|
|
||||||
|
vertical(width, y) {
|
||||||
|
return d3.svg.axis().scale(y)
|
||||||
|
.orient("left")
|
||||||
|
.tickSize(-width)
|
||||||
|
.ticks(5)
|
||||||
|
.tickPadding(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
149
src/js/modules/chart/lines.js
Normal file
149
src/js/modules/chart/lines.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import d3 from 'd3';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import config from '../../models/config.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
// A graph of closed issues.
|
||||||
|
// `issues`: closed issues list
|
||||||
|
// `created_at`: milestone start date
|
||||||
|
// `total`: total number of points (open & closed issues)
|
||||||
|
actual(issues, created_at, total) {
|
||||||
|
let min, max;
|
||||||
|
let head = [{
|
||||||
|
'date': moment(created_at, moment.ISO_8601).toJSON(),
|
||||||
|
'points': total
|
||||||
|
}];
|
||||||
|
|
||||||
|
min = +Infinity , max = -Infinity;
|
||||||
|
|
||||||
|
// Generate the actual closes.
|
||||||
|
let rest = _.map(issues, (issue) => {
|
||||||
|
let { size, closed_at } = issue;
|
||||||
|
// Determine the range.
|
||||||
|
if (size < min) min = size;
|
||||||
|
if (size > max) max = size;
|
||||||
|
|
||||||
|
// Dropping points remaining.
|
||||||
|
issue.date = moment(closed_at, moment.ISO_8601).toJSON();
|
||||||
|
issue.points = total -= size;
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now add a radius in a range (will be used for a circle).
|
||||||
|
let range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ]);
|
||||||
|
|
||||||
|
rest = _.map(rest, (issue) => {
|
||||||
|
issue.radius = range(issue.size);
|
||||||
|
return issue;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [].concat(head, rest);
|
||||||
|
},
|
||||||
|
|
||||||
|
// A graph of an ideal progression..
|
||||||
|
// `a`: milestone start date
|
||||||
|
// `b`: milestone end date
|
||||||
|
// `total`: total number of points (open & closed issues)
|
||||||
|
ideal(a, b, total) {
|
||||||
|
// Swap if end is before the start...
|
||||||
|
if (b < a) [ b, a ] = [ a, b ];
|
||||||
|
|
||||||
|
a = moment(a, moment.ISO_8601);
|
||||||
|
// Do we have a due date?
|
||||||
|
b = (b != null) ? moment(b, moment.ISO_8601) : moment.utc();
|
||||||
|
|
||||||
|
// Go through the beginning to the end skipping off days.
|
||||||
|
let days = [], length = 0, once;
|
||||||
|
(once = (inc) => {
|
||||||
|
// A new day. TODO: deal with hours and minutes!
|
||||||
|
let day = a.add(1, 'days');
|
||||||
|
|
||||||
|
// Does this day count?
|
||||||
|
let day_of;
|
||||||
|
if (!(day_of = day.weekday())) day_of = 7;
|
||||||
|
|
||||||
|
if (config.chart.off_days.indexOf(day_of) != -1) {
|
||||||
|
days.push({ 'date': day.toJSON(), 'off_day': true });
|
||||||
|
} else {
|
||||||
|
length += 1;
|
||||||
|
days.push({ 'date': day.toJSON() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go again?
|
||||||
|
if (!(day > b)) once(inc + 1);
|
||||||
|
})(0);
|
||||||
|
|
||||||
|
// Map points on the array of days now.
|
||||||
|
let velocity = total / (length - 1);
|
||||||
|
|
||||||
|
days = _.map(days, (day, i) => {
|
||||||
|
day.points = total;
|
||||||
|
if (days[i] && !days[i].off_day) total -= velocity;
|
||||||
|
return day;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do we need to make a link to right now?
|
||||||
|
let now;
|
||||||
|
if ((now = moment.utc()) > b) {
|
||||||
|
days.push({ 'date': now.toJSON(), 'points': 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Graph representing a trendling of actual issues.
|
||||||
|
trend(actual, created_at, due_on) {
|
||||||
|
if (!actual.length) return [];
|
||||||
|
|
||||||
|
let first = actual[0], last = actual[actual.length - 1];
|
||||||
|
|
||||||
|
let start = moment(first.date, moment.ISO_8601);
|
||||||
|
|
||||||
|
// Values is a list of time from the start and points remaining.
|
||||||
|
let values = _.map(actual, ({ date, points }) => {
|
||||||
|
return [ moment(date, moment.ISO_8601).diff(start), points ];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now is an actual point too.
|
||||||
|
let now = moment.utc();
|
||||||
|
values.push([ now.diff(start), last.points ]);
|
||||||
|
|
||||||
|
// http://classroom.synonym.com/calculate-trendline-2709.html
|
||||||
|
let b1 = 0, e = 0, c1 = 0, l = values.length;
|
||||||
|
let a = l * _.reduce(values, (sum, [ a, b ]) => {
|
||||||
|
b1 += a; e += b;
|
||||||
|
c1 += Math.pow(a, 2);
|
||||||
|
return sum + (a * b);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
let slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2)));
|
||||||
|
let intercept = (e - (slope * b1)) / l;
|
||||||
|
|
||||||
|
let fn = (x) => slope * x + intercept;
|
||||||
|
|
||||||
|
// Milestone always has a creation date.
|
||||||
|
created_at = moment(created_at, moment.ISO_8601);
|
||||||
|
|
||||||
|
// Due date specified.
|
||||||
|
if (due_on) {
|
||||||
|
due_on = moment(due_on, moment.ISO_8601);
|
||||||
|
// In the past?
|
||||||
|
if (now > due_on) due_on = now;
|
||||||
|
// No due date
|
||||||
|
} else {
|
||||||
|
due_on = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = created_at.diff(start);
|
||||||
|
let b = due_on.diff(start);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ 'date': created_at.toJSON(), 'points': fn(a) },
|
||||||
|
{ 'date': due_on.toJSON(), 'points': fn(b) }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -28,10 +28,7 @@ let defaults = {
|
|||||||
export default {
|
export default {
|
||||||
|
|
||||||
// Get a repo.
|
// Get a repo.
|
||||||
repo: (user, args, cb) => {
|
repo: (user, { owner, name }, cb) => {
|
||||||
if (!isValid(args)) return cb('Request is malformed');
|
|
||||||
let { owner, name } = args;
|
|
||||||
|
|
||||||
let token = (user && user.github != null) ? user.github.accessToken : null;
|
let token = (user && user.github != null) ? user.github.accessToken : null;
|
||||||
let data = _.defaults({
|
let data = _.defaults({
|
||||||
'path': `/repos/${owner}/${name}`,
|
'path': `/repos/${owner}/${name}`,
|
||||||
@ -42,10 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Get all open milestones.
|
// Get all open milestones.
|
||||||
allMilestones: (user, args, cb) => {
|
allMilestones: (user, { owner, name }, cb) => {
|
||||||
if (!isValid(args)) return cb('Request is malformed');
|
|
||||||
let { owner, name } = args;
|
|
||||||
|
|
||||||
let token = (user && user.github != null) ? user.github.accessToken : null;
|
let token = (user && user.github != null) ? user.github.accessToken : null;
|
||||||
let data = _.defaults({
|
let data = _.defaults({
|
||||||
'path': `/repos/${owner}/${name}/milestones`,
|
'path': `/repos/${owner}/${name}/milestones`,
|
||||||
@ -57,10 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Get one open milestone.
|
// Get one open milestone.
|
||||||
oneMilestone: (user, args, cb) => {
|
oneMilestone: (user, { owner, name, milestone }, cb) => {
|
||||||
if (!isValid(args)) return cb('Request is malformed');
|
|
||||||
let { owner, name, milestone } = args;
|
|
||||||
|
|
||||||
let token = (user && user.github != null) ? user.github.accessToken : null;
|
let token = (user && user.github != null) ? user.github.accessToken : null;
|
||||||
let data = _.defaults({
|
let data = _.defaults({
|
||||||
'path': `/repos/${owner}/${name}/milestones/${milestone}`,
|
'path': `/repos/${owner}/${name}/milestones/${milestone}`,
|
||||||
@ -72,10 +63,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Get all issues for a state..
|
// Get all issues for a state..
|
||||||
allIssues: (user, args, query, cb) => {
|
allIssues: (user, { owner, name, milestone }, query, cb) => {
|
||||||
if (!isValid(args)) return cb('Request is malformed');
|
|
||||||
let { owner, name, milestone } = args;
|
|
||||||
|
|
||||||
let token = (user && user.github != null) ? user.github.accessToken : null;
|
let token = (user && user.github != null) ? user.github.accessToken : null;
|
||||||
let data = _.defaults({
|
let data = _.defaults({
|
||||||
'path': `/repos/${owner}/${name}/issues`,
|
'path': `/repos/${owner}/${name}/issues`,
|
||||||
@ -143,24 +131,6 @@ let headers = (token) => {
|
|||||||
return h;
|
return h;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate args.
|
|
||||||
let isValid = (obj) => {
|
|
||||||
let rules = {
|
|
||||||
owner: (x) => { return (typeof x !== "undefined" && x !== null); },
|
|
||||||
name: (x) => { return (typeof x !== "undefined" && x !== null); },
|
|
||||||
milestone: (x) => { return _.isInt(x); } // mixin
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let key in obj) {
|
|
||||||
let val = obj[key];
|
|
||||||
if (key in rules && !rules[key](val)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse an error.
|
// Parse an error.
|
||||||
let error = (err) => {
|
let error = (err) => {
|
||||||
let text, type;
|
let text, type;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import Page from '../mixins/Page.js';
|
import Page from '../mixins/Page.js';
|
||||||
|
|
||||||
import Notify from '../components/Notify.jsx';
|
import Notify from '../components/Notify.jsx';
|
||||||
import Header from '../components/Header.jsx';
|
import Header from '../components/Header.jsx';
|
||||||
|
import Chart from '../components/Chart.jsx';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
|
|
||||||
@ -12,12 +14,35 @@ export default React.createClass({
|
|||||||
mixins: [ Page ],
|
mixins: [ Page ],
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let content;
|
||||||
|
if (!this.state.app.loading) {
|
||||||
|
let projects = this.state.projects;
|
||||||
|
// Find the milestone.
|
||||||
|
let milestone;
|
||||||
|
_.find(projects.list, (obj) => {
|
||||||
|
if (obj.owner == this.props.owner && obj.name == this.props.name) {
|
||||||
|
return _.find(obj.milestones, (m) => {
|
||||||
|
if (m.number == this.props.milestone) {
|
||||||
|
milestone = m;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (milestone) {
|
||||||
|
content = <Chart milestone={milestone} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Notify />
|
<Notify />
|
||||||
<Header {...this.state} />
|
<Header {...this.state} />
|
||||||
|
|
||||||
<div id="page" />
|
<div id="page">{content}</div>
|
||||||
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div className="wrap">
|
<div className="wrap">
|
||||||
|
@ -5,7 +5,6 @@ import Page from '../mixins/Page.js';
|
|||||||
import Notify from '../components/Notify.jsx';
|
import Notify from '../components/Notify.jsx';
|
||||||
import Header from '../components/Header.jsx';
|
import Header from '../components/Header.jsx';
|
||||||
import Milestones from '../components/Milestones.jsx';
|
import Milestones from '../components/Milestones.jsx';
|
||||||
import Hero from '../components/Hero.jsx';
|
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
|
|
||||||
|
@ -59,61 +59,86 @@ class ProjectsStore extends Store {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch milestones and issues for a project(s).
|
// Fetch milestones and issues for a project.
|
||||||
onProjectsLoad(project) {
|
getProject(user, p) {
|
||||||
|
// Fetch their milestones.
|
||||||
|
milestones.fetchAll(user, p, this.cb((err, milestones) => { // async
|
||||||
|
// Save the error if project does not exist.
|
||||||
|
if (err) return this.saveError(p, err);
|
||||||
|
// Now add in the issues.
|
||||||
|
milestones.forEach((milestone) => {
|
||||||
|
// Do we have this milestone already? Skip fetching issues then.
|
||||||
|
if (!_.find(p.milestones, ({ number }) => {
|
||||||
|
return milestone.number === number;
|
||||||
|
})) {
|
||||||
|
// Fetch all the issues for this milestone.
|
||||||
|
this.getIssues(user, p, milestone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch a single milestone.
|
||||||
|
getMilestone(user, p, m) {
|
||||||
|
// Fetch the single milestone.
|
||||||
|
milestones.fetch(user, {
|
||||||
|
'owner': p.owner,
|
||||||
|
'name': p.name,
|
||||||
|
'milestone': m
|
||||||
|
}, this.cb((err, milestone) => { // async
|
||||||
|
// Save the error if project does not exist.
|
||||||
|
if (err) return this.saveError(p, err);
|
||||||
|
// Now add in the issues.
|
||||||
|
this.getIssues(user, p, milestone);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all issues for a milestone.
|
||||||
|
getIssues(user, p, m) {
|
||||||
|
issues.fetchAll(user, {
|
||||||
|
'owner': p.owner,
|
||||||
|
'name': p.name,
|
||||||
|
'milestone': m.number
|
||||||
|
}, this.cb((err, obj) => { // async
|
||||||
|
// Save any errors on the project.
|
||||||
|
if (err) return this.saveError(p, err);
|
||||||
|
// Add in the issues to the milestone.
|
||||||
|
_.extend(m, { 'issues': obj });
|
||||||
|
// Save the milestone.
|
||||||
|
this.addMilestone(p, m);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch milestone(s) and issues for a project(s).
|
||||||
|
onProjectsLoad(args) {
|
||||||
let projects = this.get('list');
|
let projects = this.get('list');
|
||||||
|
|
||||||
// Quit if we have no projects.
|
// Quit if we have no projects.
|
||||||
if (!projects.length) return;
|
if (!projects.length) return;
|
||||||
|
|
||||||
// Fetch milestones and issues for a project.
|
|
||||||
let get = (user, p) => {
|
|
||||||
// Fetch their milestones.
|
|
||||||
milestones.fetchAll(user, p, this.cb((err, milestones) => { // async
|
|
||||||
// Save the error if project does not exist.
|
|
||||||
if (err) return this.saveError(p, err);
|
|
||||||
// Now add in the issues.
|
|
||||||
milestones.forEach((milestone) => {
|
|
||||||
// Do we have this milestone already? Skip fetching issues then.
|
|
||||||
if (_.find(p.milestones, ({ number }) => {
|
|
||||||
return milestone.number === number;
|
|
||||||
})) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK fetch all the issues for this milestone then.
|
|
||||||
issues.fetchAll(user, {
|
|
||||||
'owner': p.owner,
|
|
||||||
'name': p.name,
|
|
||||||
'milestone': milestone.number
|
|
||||||
}, this.cb((err, obj) => { // async
|
|
||||||
// Save any errors on the project.
|
|
||||||
if (err) return this.saveError(p, err);
|
|
||||||
// Add in the issues to the milestone.
|
|
||||||
_.extend(milestone, { 'issues': obj });
|
|
||||||
// Save the milestone.
|
|
||||||
this.addMilestone(p, milestone);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wait for the user to get resolved.
|
// Wait for the user to get resolved.
|
||||||
this.get('user', this.cb((user) => { // async
|
this.get('user', this.cb((user) => { // async
|
||||||
if (project) {
|
if (args) {
|
||||||
// For a single project.
|
if ('milestone' in args) {
|
||||||
_.find(this.get('list'), (obj) => {
|
// For a single milestone.
|
||||||
if (project.owner == obj.owner && project.name == obj.name) {
|
this.getMilestone(user, {
|
||||||
project = obj; // expand by saved properties
|
'owner': args.owner,
|
||||||
return true;
|
'name': args.name
|
||||||
};
|
}, args.milestone);
|
||||||
return false;
|
} else {
|
||||||
});
|
// For a single project.
|
||||||
// For a single project.
|
_.find(this.get('list'), (obj) => {
|
||||||
get(user, project);
|
if (args.owner == obj.owner && args.name == obj.name) {
|
||||||
|
args = obj; // expand by saved properties
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
this.getProject(user, args);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// For all projects.
|
// For all projects.
|
||||||
projects.forEach(_.partial(get, user));
|
projects.forEach(_.partial(this.getProject, user));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
56
test/lines.js
Normal file
56
test/lines.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
import moment from 'moment';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import lines from '../src/js/modules/chart/lines.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'lines - actual': (done) => {
|
||||||
|
let issues = [
|
||||||
|
{ 'size': 3, 'date': 2 },
|
||||||
|
{ 'size': 2, 'date': 3 },
|
||||||
|
{ 'size': 1, 'date': 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let points = _.map(lines.actual(issues, 1, 6), ({ points }) => points);
|
||||||
|
|
||||||
|
assert.deepEqual(points, [6, 3, 1, 0]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
|
||||||
|
'lines - ideal': (done) => {
|
||||||
|
let a = '2011-04-01T00:00:00Z';
|
||||||
|
let b = '2011-04-03T00:00:00Z';
|
||||||
|
|
||||||
|
let line = lines.ideal(a, b, 4).slice(0, 3);
|
||||||
|
|
||||||
|
assert.deepEqual(line, [
|
||||||
|
{ '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 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
|
||||||
|
'lines - trend': (done) => {
|
||||||
|
let issues = [
|
||||||
|
{ 'date': '2011-04-02T00:00:00.000Z', 'points': 4 },
|
||||||
|
{ 'date': '2011-04-03T00:00:00.000Z', 'points': 1 },
|
||||||
|
{ 'date': '2011-04-04T00:00:00.000Z', 'points': 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let opts = [
|
||||||
|
issues,
|
||||||
|
'2011-04-02T00:00:00.000Z',
|
||||||
|
moment.utc()
|
||||||
|
];
|
||||||
|
|
||||||
|
let line = _.map(lines.trend.apply(null, opts), ({ points }) => Math.round(points));
|
||||||
|
|
||||||
|
assert.deepEqual(line, [2, 1]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user