Skip to content

Instantly share code, notes, and snippets.

@erochest
Created January 18, 2018 13:56
Show Gist options
  • Save erochest/d3ea97a455710b6d7c0aa4767c1711ef to your computer and use it in GitHub Desktop.
Save erochest/d3ea97a455710b6d7c0aa4767c1711ef to your computer and use it in GitHub Desktop.
Tracker Iteration Burnup
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Iteration Burnup</title>
<link rel="stylesheet" href="master.css" type="text/css" media="screen" title="no title" charset="utf-8">
</head>
<body>
<form>
<label for="api_token">API Token</label>
<input type="text" id="api_token" value="6e1726ef5af3e25ee3a792f5613047d3"/>
<label for="project_id">Project ID</label>
<input type="text" id="project_id" value="2098949"/>
<button id="submit_chart">Chart</button>
</form>
<svg id="container" width="750" height="340"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-time.v1.min.js"></script>
<script src="https://d3js.org/d3-time-format.v2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
<script src="main.js" charset="utf-8"></script>
</body>
</html>
document.addEventListener("DOMContentLoaded", function() {
const pivotalApi = "https://www.pivotaltracker.com/services/v5";
const reportFields = new Set([
"points_accepted", "points_rejected", "points_delivered", "points_finished",
"points_started", "points_unstarted"
]);
const getCurrentIteration = (apiToken, projectId) => {
const url = `${pivotalApi}/projects/${projectId}?fields=current_iteration_number`;
const init = {
method: 'GET',
headers: {'X-TrackerToken': apiToken}
};
console.log('fetching current iteration');
return fetch(url, init)
.then((response) => response.json())
.then((json) => json.current_iteration_number);
};
const getIterationHistory = (apiToken, projectId, iterationNumber) => {
const url = `${pivotalApi}/projects/${projectId}/history/iterations/${iterationNumber}/days`;
const init = {
method: 'GET',
headers: {'X-TrackerToken': apiToken}
};
console.log('fetching iteration history');
return fetch(url, init)
.then((response) => response.json());
};
const objectify = (header, row) => {
const accum = (o, v, i) => {
o[v] = row[i];
return o;
};
const sumReportFields = (s, v, i) => {
if (reportFields.has(v)) {
s += row[i];
}
return s;
};
let obj = header.reduce(accum, {});
let total = header.reduce(sumReportFields, 0);
obj.total = total;
return obj;
}
const legendLabel = key => {
const label = key.split('_')[1];
return label[0].toUpperCase() + label.substring(1);
};
const columnLabel = key => {
const day = moment(key);
return day.format('MMM D');
};
const chartBurnUp = (iterationData) => {
var {header, data} = iterationData;
var objects = data.map(row => objectify(header, row));
var svg = d3.select('#container'),
margin = {top: 42, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
const legendColumn = 40;
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.4)
.align(0.1);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(["#73cb34", "#e02626", "#ffad13", "#849ecd", "#f3e958", "#cccdcf"]);
var keys = ["points_accepted", "points_rejected", "points_delivered",
"points_finished", "points_started", "points_unstarted"];
x.domain(objects.map(o => o.date));
y.domain([0, d3.max(objects, o => o.total)]).nice();
z.domain(keys);
g.append('g')
.selectAll('g')
.data(d3.stack().keys(keys)(objects))
.enter().append('g')
.attr('fill', d => z(d.key))
.selectAll('rect')
.data(d => d)
.enter().append('rect')
.attr('x', d => x(d.data.date))
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
.attr('width', x.bandwidth());
const xAxis = d3.axisBottom(x)
.tickFormat(d => moment(d).format('MMM D'));
g.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
g.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y).ticks(null, 's'))
.append('text')
.attr('x', 2)
.attr('y', y(y.ticks().pop()) + 0.5)
.attr('dy', '0.32em')
.attr('fill', '#000')
/*
*.attr('font-weight', 'bold')
*.attr('text-anchor', 'start')
*.text('Points')
*/
;
// TODO: dotted lines going across the background
// TODO: fill in empty data for today and future dates
var legend = g.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.attr('transform', 'translate(0, -30)')
.selectAll('g')
.data(keys)
.enter().append('g')
.attr('transform', (d, i) => `translate(${i * legendColumn}, 0)`);
legend.append('rect')
.attr('x', (d, i) => 50 + i * legendColumn)
.attr('width', 19)
.attr('height', 19)
.attr('fill', z);
legend.append('text')
.attr('x', (d, i) => 50 + i * legendColumn - 5)
.attr('y', 9.5)
.attr('dy', '0.32em')
.text(legendLabel);
};
document.getElementById("submit_chart").addEventListener('click', (event) => {
const apiToken = document.getElementById("api_token").value;
const projectId = document.getElementById("project_id").value;
event.preventDefault();
getCurrentIteration(apiToken, projectId)
.then(iterationNumber => getIterationHistory(apiToken, projectId, iterationNumber))
.then(chartBurnUp);
});
});
#container {
height: 340px;
width: 750px;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment