|
<!DOCTYPE html> |
|
<html> |
|
<meta charset="utf-8"> |
|
<head> |
|
<title>Estimated Timeline</title> |
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<link rel="stylesheet" href="http://billmill.org/css/style.css" /> |
|
<style> |
|
body { |
|
font-family: Georgia,"Times New Roman",Times,serif; |
|
font-size: 14px; |
|
background-color: white; |
|
width: 1020px; |
|
} |
|
table { |
|
border-collapse: collapse; |
|
} |
|
th { |
|
border-bottom: 2px solid #ddd; |
|
padding: 8px; |
|
font-weight: bold; |
|
} |
|
td { |
|
padding: 8px; |
|
border-top: 1px solid #ddd; |
|
} |
|
#chart { |
|
padding: 0px; |
|
} |
|
.xaxislabel { |
|
font-size: 9px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div id="datatable"></div> |
|
|
|
<script> |
|
|
|
var rows = [] |
|
var formatdate = d3.time.format("%b %d %Y"); |
|
|
|
d3.csv("estimates.csv", function(error, csv) { |
|
csv.forEach(function(row) { |
|
row.startAt = new Date(Date.parse(row.startAt)); |
|
row.dueAt = new Date(Date.parse(row.dueAt)); |
|
row.expectedAt = new Date(Date.parse(row.expectedAt)); |
|
rows.push(row); |
|
}); |
|
|
|
var table = d3.select("#datatable").append("table"); |
|
thead = table.append("thead"); |
|
tbody = table.append("tbody"); |
|
|
|
thead.append("th").text("Description"); |
|
thead.append("th").text("Start At"); |
|
thead.append("th").text("Due At"); |
|
thead.append("th").text("Expected At"); |
|
thead.append("th").text(""); |
|
|
|
var tr = tbody.selectAll("tr") |
|
.data(rows) |
|
.enter().append("tr"); |
|
|
|
var td = tr.selectAll("td") |
|
.data(function(d) { return [d.description, formatdate(d.startAt), formatdate(d.dueAt), formatdate(d.expectedAt)]; }) |
|
.enter().append("td") |
|
.text(function(d) { return d; }); |
|
|
|
var width = 160, |
|
height = d3.select("table")[0][0].clientHeight, |
|
mx = 10, |
|
radius = 4; |
|
|
|
// Now add the chart column |
|
d3.select("#datatable tbody tr").append("td") |
|
.attr("id", "chart") |
|
.attr("width", width + "px") |
|
.attr("rowspan", rows.length); |
|
|
|
var chart = d3.select("#chart").append("svg") |
|
.attr("class", "chart") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
var maxDate = new Date(Date.parse("January 1, 2014")); |
|
var minDate = new Date(Date.parse("December 31, 2016")); |
|
for (i=0; i < rows.length; i++) { |
|
if (rows[i].expectedAt > maxDate) { maxDate = rows[i].expectedAt; } |
|
if (rows[i].dueAt > maxDate) { maxDate = rows[i].dueAt; } |
|
if (rows[i].startAt < minDate) { minDate = rows[i].startAt; } |
|
} |
|
|
|
var seqs = rows.map(function(t) { return t.seq; }); |
|
|
|
var xscale = d3.time.scale() |
|
.domain([minDate, maxDate]) |
|
.range([mx, width-mx]) |
|
.nice(); |
|
|
|
var yscale = d3.scale.ordinal() |
|
.domain(seqs) |
|
.rangeBands([0, height]); |
|
|
|
// FIXME: Get a better x axis |
|
// chart.selectAll(".xaxislabel") |
|
// .data(xscale.ticks(2)) |
|
// .enter().append("text") |
|
// .attr("class", "xaxislabel") |
|
// .attr("x", function(d) { return xscale(d); }) |
|
// .attr("y", 10) |
|
// .attr("text-anchor", "middle") |
|
// .text(String) |
|
|
|
chart.selectAll(".xaxistick") |
|
.data(xscale.ticks(2)) |
|
.enter().append("line") |
|
.attr("x1", function(d) { return xscale(d); }) |
|
.attr("x2", function(d) { return xscale(d); }) |
|
.attr("y1", 10) |
|
.attr("y2", height) |
|
.attr("stroke", "#eee") |
|
.attr("stroke-width", 1); |
|
|
|
chart.selectAll(".line") |
|
.data(rows) |
|
.enter().append("line") |
|
.attr("x1", function(d) { return xscale(d.startAt); }) |
|
.attr("y1", function(d) { return yscale(d.seq) + yscale.rangeBand()/2; }) |
|
.attr("x2", function(d) { return xscale(d.expectedAt); }) |
|
.attr("y2", function(d) { return yscale(d.seq) + yscale.rangeBand()/2; }) |
|
.attr("stroke", "#888") |
|
.attr("stroke-width", 8); |
|
|
|
var pt = chart.selectAll(".pt") |
|
.data(rows) |
|
.enter().append("g") |
|
.attr("class", "pt") |
|
.attr("transform", function(d) { return "translate(" + xscale(d.startAt) + "," + (yscale(d.seq) + yscale.rangeBand()/2) + ")"; }); |
|
|
|
pt.append("circle") |
|
.attr("cx", -5) |
|
.attr("cy", 0) |
|
.attr("r", radius) |
|
.attr("opacity", function(d) { return d.expectedAt > d.dueAt ? .6 : 0 }) |
|
.attr("fill", "#ff0000"); |
|
|
|
chart.selectAll(".due-tick") |
|
.data(rows) |
|
.enter().append("line") |
|
.attr("x1", function(d) { return xscale(d.dueAt); }) |
|
.attr("y1", function(d) { return yscale(d.seq) + yscale.rangeBand()/2 - 6; }) |
|
.attr("x2", function(d) { return xscale(d.dueAt); }) |
|
.attr("y2", function(d) { return yscale(d.seq) + yscale.rangeBand()/2 + 6; }) |
|
.attr("stroke", "#555") |
|
.attr("stroke-width", 3); |
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |