show a chart
This commit is contained in:
parent
56aedfeb23
commit
3762507c27
|
@ -6,6 +6,9 @@ d3Tip(d3);
|
|||
|
||||
import Format from '../mixins/Format.js';
|
||||
|
||||
import lines from '../modules/chart/lines.js';
|
||||
import axes from '../modules/chart/axes.js';
|
||||
|
||||
export default React.createClass({
|
||||
|
||||
displayName: 'Chart.jsx',
|
||||
|
@ -32,7 +35,140 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount() {
|
||||
console.log(this.refs);
|
||||
let milestone = this.props.milestone,
|
||||
issues = milestone.issues;
|
||||
// Total number of points in the milestone.
|
||||
let total = issues.open.size + issues.closed.size;
|
||||
|
||||
// An issue may have been closed before the start of a milestone.
|
||||
let head = issues.closed.list[0].closed_at;
|
||||
if (issues.length && milestone.created_at > head) {
|
||||
// This is the new start.
|
||||
milestone.created_at = head;
|
||||
}
|
||||
|
||||
// Actual, ideal & trend lines.
|
||||
let actual = lines.actual(issues.closed.list, milestone.created_at, total);
|
||||
let ideal = lines.ideal(milestone.created_at, milestone.due_on, total);
|
||||
let trend = lines.trend(actual, milestone.created_at, milestone.due_on);
|
||||
|
||||
// Get available space.
|
||||
let { height, width } = this.refs.el.getBoundingClientRect();
|
||||
|
||||
let margin = { 'top': 30, 'right': 30, 'bottom': 40, 'left': 50 };
|
||||
width -= margin.left + margin.right;
|
||||
height -= margin.top + margin.bottom;
|
||||
|
||||
// Scales.
|
||||
let x = d3.time.scale().range([ 0, width ]);
|
||||
let y = d3.scale.linear().range([ height, 0 ]);
|
||||
|
||||
// Axes.
|
||||
let xAxis = axes.horizontal(height, x);
|
||||
let yAxis = axes.vertical(width, y);
|
||||
|
||||
// Line generator.
|
||||
let line = d3.svg.line()
|
||||
.interpolate("linear")
|
||||
.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.
|
||||
let first = ideal[0], last = ideal[ideal.length - 1];
|
||||
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.
|
||||
let svg = d3.select(this.refs.el).append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// Add the clip path so that lines are not drawn outside of the boundary.
|
||||
svg.append("defs").append("svg:clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("svg:rect")
|
||||
.attr("id", "clip-rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
// Add the days x-axis.
|
||||
svg.append("g")
|
||||
.attr("class", "x axis day")
|
||||
.attr("transform", `translate(0,${height})`)
|
||||
.call(xAxis);
|
||||
|
||||
// Add the months x-axis.
|
||||
let m = [
|
||||
'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")
|
||||
.attr("class", "x axis month")
|
||||
.attr("transform", `translate(0,${height})`)
|
||||
.call(mAxis);
|
||||
|
||||
// Add the y-axis.
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.call(yAxis);
|
||||
|
||||
// Add a line showing where we are now.
|
||||
svg.append("svg:line")
|
||||
.attr("class", "today")
|
||||
.attr("x1", x(new Date))
|
||||
.attr("y1", 0)
|
||||
.attr("x2", x(new Date))
|
||||
.attr("y2", height);
|
||||
|
||||
// Add the ideal line path.
|
||||
svg.append("path")
|
||||
.attr("class", "ideal line")
|
||||
// Piecewise linear segments, as in a polyline.
|
||||
.attr("d", line.interpolate("linear")(ideal));
|
||||
|
||||
// Add the trendline path.
|
||||
svg.append("path")
|
||||
.attr("class", "trendline line")
|
||||
// Piecewise linear segments, as in a polyline.
|
||||
.attr("d", line.interpolate("linear")(trend));
|
||||
|
||||
// Add the actual line path.
|
||||
svg.append("path")
|
||||
.attr("class", "actual line")
|
||||
// Piecewise linear segments, as in a polyline.
|
||||
.attr("d", line.interpolate("linear").y((d) => y(d.points))(actual));
|
||||
|
||||
// Collect the tooltip here.
|
||||
let tooltip = d3.tip().attr('class', 'd3-tip')
|
||||
.html(({ number, title }) => `#${number}: ${title}`);
|
||||
|
||||
svg.call(tooltip);
|
||||
|
||||
// Show when we closed an issue.
|
||||
svg.selectAll("a.issue")
|
||||
.data(actual.slice(1)) // skip the starting point
|
||||
.enter()
|
||||
// A wrapping link.
|
||||
.append('svg:a')
|
||||
.attr("xlink:href", ({ html_url }) => html_url )
|
||||
.attr("xlink:show", 'new')
|
||||
.append('svg:circle')
|
||||
.attr("cx", ({ date }) => x(new Date(date)))
|
||||
.attr("cy", ({ points }) => y(points))
|
||||
.attr("r", ({ radius }) => 5)
|
||||
.on('mouseover', tooltip.show)
|
||||
.on('mouseout', tooltip.hide);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -59,56 +59,6 @@ class ProjectsStore extends Store {
|
|||
});
|
||||
}
|
||||
|
||||
// Fetch milestones and issues for a 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');
|
||||
|
@ -138,7 +88,7 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
} else {
|
||||
// For all projects.
|
||||
projects.forEach(_.partial(this.getProject, user));
|
||||
_.each(projects, _.partial(this.getProject, user), this);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@ -244,8 +194,57 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch milestones and issues for a 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);
|
||||
}));
|
||||
}
|
||||
|
||||
// Add a milestone for a project.
|
||||
// TODO: check if this milestone exists already.
|
||||
addMilestone(project, milestone) {
|
||||
// Add in the stats.
|
||||
let i, j;
|
||||
|
@ -253,6 +252,16 @@ class ProjectsStore extends Store {
|
|||
// We are supposed to exist already.
|
||||
if ((i = this.findIndex(project)) < 0) { throw 500; }
|
||||
|
||||
// Does the milestone exist already?
|
||||
let milestones;
|
||||
if (milestones = this.get(`list.${i}.milestones`)) {
|
||||
j = _.findIndex(milestones, { 'number': milestone.number });
|
||||
// Just make an update then.
|
||||
if (j != -1) {
|
||||
return this.set(`list.${i}.milestones.${j}`, milestone);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the milestone and return the index.
|
||||
j = this.push(`list.${i}.milestones`, milestone);
|
||||
|
||||
|
|
Loading…
Reference in New Issue