show a chart

This commit is contained in:
Radek Stepan 2016-01-20 16:00:55 +01:00
parent 56aedfeb23
commit 3762507c27
2 changed files with 198 additions and 53 deletions

View File

@ -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);
}
});

View File

@ -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);