more intelligent time ticks; closes #65

This commit is contained in:
Radek Stepan 2016-01-26 23:40:50 +01:00
parent 687a32e7de
commit e3953f6ce5
6 changed files with 113 additions and 88 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "burnchart", "name": "burnchart",
"version": "3.0.0", "version": "3.0.1",
"description": "GitHub Burndown Chart as a Service", "description": "GitHub Burndown Chart as a Service",
"author": "Radek Stepan <dev@radekstepan.com> (http://radekstepan.com)", "author": "Radek Stepan <dev@radekstepan.com> (http://radekstepan.com)",
"license": "AGPL-3.0", "license": "AGPL-3.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -67,8 +67,8 @@ export default React.createClass({
let y = d3.scale.linear().range([ height, 0 ]); let y = d3.scale.linear().range([ height, 0 ]);
// Axes. // Axes.
let xAxis = axes.horizontal(height, x); let xAxis = axes.time(height, x, milestone.stats.span);
let yAxis = axes.vertical(width, y); let yAxis = axes.points(width, y);
// Line generator. // Line generator.
let line = d3.svg.line() let line = d3.svg.line()
@ -104,22 +104,13 @@ export default React.createClass({
.attr("transform", `translate(0,${height})`) .attr("transform", `translate(0,${height})`)
.call(xAxis); .call(xAxis);
// Add the months x-axis. // Add the years x-axis?
let m = [ let yrAxis = axes.year(height, xAxis, milestone.stats.span);
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
let mAxis = xAxis
.orient("top")
.tickSize(height)
.tickFormat((d) => m[d.getMonth()])
.ticks(2);
svg.append("g") svg.append("g")
.attr("class", "x axis month") .attr("class", "x axis year")
.attr("transform", `translate(0,${height})`) .attr("transform", `translate(0,${height})`)
.call(mAxis); .call(yrAxis);
// Add the y-axis. // Add the y-axis.
svg.append("g") svg.append("g")

View File

@ -1,19 +1,32 @@
import d3 from 'd3'; import d3 from 'd3';
import _ from 'lodash';
export default { export default {
horizontal(height, x) { time(height, x, span) {
// Tick time format based on number of days we display.
let specifier = (span < 4) ? '' : (span < 14) ? '%a' : (span < 32) ? '%m/%d' : '%b';
let format = d3.time.format.utc(specifier);
return d3.svg.axis().scale(x) return d3.svg.axis().scale(x)
.orient("bottom") .orient("bottom")
// Show vertical lines... // Show vertical lines...
.tickSize(-height) .tickSize(-height)
// with day of the month... // tick time format...
.tickFormat((d) => { return d.getDate(); }) .tickFormat(format)
// and give us a spacer. // and give us a spacer.
.tickPadding(10); .tickPadding(10);
}, },
vertical(width, y) { year(height, xAxis, span) {
return xAxis
.orient("top")
.tickSize(height)
.tickFormat(d3.time.format.utc('%Y'))
.ticks(span / 365);
},
points(width, y) {
return d3.svg.axis().scale(y) return d3.svg.axis().scale(y)
.orient("left") .orient("left")
.tickSize(-width) .tickSize(-width)

View File

@ -16,17 +16,23 @@ export default (milestone) => {
// Makes testing easier... // Makes testing easier...
if (milestone.stats != null) return milestone.stats; if (milestone.stats != null) return milestone.stats;
let isDone = false, isOnTime = true, isOverdue = false, let points = 0, a, b, c, time, days, span;
isEmpty = true, points = 0, a, b, c, time, days;
let stats = {
'isDone': false,
'isOnTime': true,
'isOverdue': false,
'isEmpty': true
};
// Progress in points. // Progress in points.
a = milestone.issues.closed.size; let i = milestone.issues.closed.size,
b = milestone.issues.open.size; j = milestone.issues.open.size;
if (a) { if (i) {
isEmpty = false; stats.isEmpty = false;
if (a + b > 0) { if (i + j > 0) {
points = progress(a, b); points = progress(i, j);
if (points === 100) isDone = true; if (points === 100) stats.isDone = true;
} }
} }
@ -37,30 +43,33 @@ export default (milestone) => {
, milestone.created_at); , milestone.created_at);
} }
// Milestones with no due date are always on track. // The dates in this milestone.
if (!(milestone.due_on != null)) {
return { isOverdue, isOnTime, isDone, isEmpty, 'progress': { points } };
}
a = moment(milestone.created_at, moment.ISO_8601); a = moment(milestone.created_at, moment.ISO_8601);
b = moment.utc(); b = moment.utc();
c = moment(milestone.due_on, moment.ISO_8601); c = moment(milestone.due_on, moment.ISO_8601);
// Milestones with no due date are always on track.
if (!(milestone.due_on != null)) {
// The number of days from start to now.
span = b.diff(a, 'days');
return _.extend(stats, { span, 'progress': { points } });
}
// Overdue? Regardless of the date, if we have closed all // Overdue? Regardless of the date, if we have closed all
// issues, we are no longer overdue. // issues, we are no longer overdue.
if (b.isAfter(c) && !isDone) isOverdue = true; if (b.isAfter(c) && !stats.isDone) stats.isOverdue = true;
// Progress in time. // Progress in time.
time = progress(b.diff(a), c.diff(b)); time = progress(b.diff(a), c.diff(b));
// How many days is 1% of the time? // Number of days between start and due date or today if overdue.
span = (stats.isOverdue ? b : c).diff(a, 'days');
// How many days is 1% of the time until now?
days = (b.diff(a, 'days')) / 100; days = (b.diff(a, 'days')) / 100;
// Are we on time?
isOnTime = points > time;
// If we have closed all issues, we are "on time". // If we have closed all issues, we are "on time".
if (isDone) isOnTime = true; stats.isOnTime = stats.isDone || points > time;
return { isOverdue, isOnTime, isDone, isEmpty, days, 'progress': { points, time } }; return _.extend(stats, { days, span, 'progress': { points, time } });
}; };