-
-
Save gesellix/c8693e1b2d4276c3492a55f65a664d7b to your computer and use it in GitHub Desktop.
Quick and dirty-ish page and script to pull annotated cards from Trello lists for creating basic burndown charts. Uses the Trello client.js and Google JSAPI APIs. General idea is that you mark lists in your board with a trailing asterisk, and annotate your cards with actions "estimate 1.5 days" or "estimate 5 hours", and then as you work, add ac…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> | |
<title>Trello Estimation Timelines</title> | |
<style type="text/css"> | |
body { | |
font-family: arial; | |
font-size: 12px; | |
} | |
#topheaderline { | |
text-align: center; | |
font-size: 40px; | |
padding-top: 30px; | |
} | |
#starting { | |
text-align: center; | |
font-size: 20px; | |
padding-top: 30px; | |
} | |
#loggedout { | |
display: none; | |
text-align: center; | |
font-size: 20px; | |
padding-top: 30px; | |
} | |
#loggedin { | |
display: none; | |
} | |
#header { | |
padding: 4px; | |
border-bottom: 1px solid #000; | |
background: #eee; | |
} | |
#output { | |
padding: 4px; | |
} | |
.board { | |
font-size: 20px; | |
display: block; | |
padding: 2px; | |
} | |
.card { | |
display: block; | |
padding: 2px; | |
} | |
</style> | |
</head> | |
<body> | |
<h1 id="topheaderline">Trello <em>Estimation</em> Timelines</h1> | |
<div id="starting"> | |
<div id="header">Connecting to Trello ...</div> | |
</div> | |
<div id="loggedout"> | |
<div id="header">Failed to connect to Trello! Check Trello API client.js key and token.</div> | |
</div> | |
<div id="loggedin"> | |
<div id="header">Logged in as <span id="fullName"></span></div> | |
<div id="output"></div> | |
</div> | |
<div id="test_dataview"></div> | |
</body> | |
<script type="text/javascript">console.log("Loading jQuery 1.7.1 ...");</script> | |
<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script> | |
<script type="text/javascript">console.log("Loading Google JSAPI API ...");</script> | |
<script type="text/javascript" src="https://www.google.com/jsapi"></script> | |
<!-- Trello API - vvvvvvvvvv Preload with application key and login user token! vvvvvvvvvv | |
Get application KEY using: https://trello.com/1/appKey/generate | |
Then get TOKEN using: https://trello.com/1/authorize?response_type=token&name=NAME&key=KEY | |
where NAME is the name you give to this "app", e.g., Burndown | |
and KEY is the Key returned by the first query | |
--> | |
<script type="text/javascript">console.log("Loading Trello API ...");</script> | |
<script type="text/javascript" src="https://api.trello.com/1/client.js?key=KEY&token=TOKEN"></script> | |
<!-- Trello API - ^^^^^^^^^^ Preload with app key and login user token! ^^^^^^^^^^ --> | |
<script type="text/javascript">//<![CDATA[ | |
// vvvvvvvvvvvvvvvvvvvv CONFIGURATION vvvvvvvvvvvvvvvvvvvv | |
// Card name upon which a Due Date sets the overall project start date. | |
// All estimate settings entered before this date form the _initial estimate_. | |
// All estimates entered after, are changes to the estimation. | |
// The start card can appear on any list, even ones not included in estimations. | |
var START_CARD = "Start Date"; | |
// The regular expression that selects the lists to include in the estimations. | |
// By default, a final '*' at the end of a list name will include all cards on that list. | |
var INCLUDE_LIST_RE = /\*$/; | |
// The regular expressions used to pull out the days of estimates and work. | |
var ESTIMATE_DAY_RE = /estimate\W*(\d+)(\.\d+)?\W*days?/i; | |
var WORK_DAY_RE = /work\W*(\d+)(\.\d+)?\W*days?/i; | |
// The regular expressions used to pull out the hours of estimates and work. | |
var ESTIMATE_HOUR_RE = /estimate\W*(\d+)(\.\d+)?\W*hours?/i; | |
var WORK_HOUR_RE = /work\W*(\d+)(\.\d+)?\W*hours?/i; | |
// How many hours are in a working day | |
var HOURS_PER_DAY = 6; | |
// ^^^^^^^^^^^^^^^^^^^^ CONFIGURATION ^^^^^^^^^^^^^^^^^^^^ | |
// vvvvvvvvvvvvvvvvvvvv IMPLEMENTATION vvvvvvvvvvvvvvvvvvvv | |
console.log("Loading Google visualization packages ..."); | |
google.load('visualization', '1.0', { 'packages': ['corechart', 'table'] }); | |
console.log("Waiting for Google visualization packages ..."); | |
google.setOnLoadCallback(authorize); | |
var BOARDS = []; | |
var $boards = $(); | |
var ISO_DATE_RE = /^(\d\d\d\d)-(\d\d)-(\d\d)T/ | |
function str2date(s) | |
{ | |
var r = ISO_DATE_RE.exec(s); | |
return new Date(parseInt(r[1], 10), parseInt(r[2], 10) - 1, parseInt(r[3], 10)); | |
} | |
function iso_date_comp(x, y) | |
{ | |
return x.date.localeCompare(y.date); | |
} | |
var OUTSTANDING_REQUESTS = 0; // This is a HACK! | |
function authorize() | |
{ | |
Trello.authorize({ | |
interactive: false, | |
success: onAuthorize | |
}); | |
} | |
function onAuthorize() | |
{ | |
if (! Trello.authorized()) { | |
$("#loggedout").toggle(true); | |
$("#loggedin").toggle(false); | |
$("#starting").toggle(false); | |
} else { | |
$("#loggedin").toggle(true); | |
$("#loggedout").toggle(false); | |
$("#starting").toggle(false); | |
$boards = $("<div>") | |
.text("Loading Boards ...") | |
.appendTo("#output"); | |
OUTSTANDING_REQUESTS += 1; | |
Trello.members.get("me", function (member) { | |
$("#fullName").text(member.fullName); | |
loadBoards(); | |
OUTSTANDING_REQUESTS -= 1; | |
}); | |
} | |
}; | |
function loadBoards() | |
{ | |
console.log("Loading Boards ..."); | |
OUTSTANDING_REQUESTS += 1; | |
Trello.members.get("me/boards", { filter: "open" }, function (boards) { | |
var i; | |
console.log("Found " + boards.length + " Boards."); | |
BOARDS = boards; | |
for (i = 0; i < boards.length; i += 1) { | |
loadLists(boards[i]); | |
}; | |
OUTSTANDING_REQUESTS -= 1; | |
}); | |
}; | |
function loadLists(board) | |
{ | |
console.log("Loading Lists for '" + board.name + "' (" + board.id + ") ..."); | |
OUTSTANDING_REQUESTS += 1; | |
Trello.boards.get(board.id + "/lists", function (lists) { | |
var i; | |
console.log("Found " + lists.length + " Lists in Board '" + board.name + "'."); | |
board.lists = lists; | |
for (i = 0; i < lists.length; i += 1) { | |
loadCards(board, lists[i], INCLUDE_LIST_RE.test(lists[i].name)); | |
}; | |
OUTSTANDING_REQUESTS -= 1; | |
}); | |
}; | |
function loadCards(board, list, include_estimates) | |
{ | |
console.log("Loading Cards for List '" + list.name + "' (" + list.id + ") in Board '" + | |
board.name + "' (" + board.id + ") " + (include_estimates && "with" || "without") + " estimates ..."); | |
OUTSTANDING_REQUESTS += 1; | |
Trello.lists.get(list.id + "/cards", function (cards) { | |
var i; | |
console.log("Found " + cards.length + " Cards in List '" + list.name + "' in Board '" + board.name + "'."); | |
list.cards = cards; | |
for (i = 0; i < cards.length; i += 1) { | |
if (START_CARD.localeCompare(cards[i].name) === 0 && cards[i].due) { | |
board.start_date = cards[i].due; | |
} | |
loadActions(board, list, cards[i], include_estimates); | |
}; | |
OUTSTANDING_REQUESTS -= 1; | |
}); | |
}; | |
var ISO_DATETIME_OVERRIDE_RE = /@@ (\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ) @@/ | |
function loadActions(board, list, card, include_estimates) | |
{ | |
console.log("Loading Actions for '" + board.name + "' " + board.id + " include_estimates=" + include_estimates + " ..."); | |
OUTSTANDING_REQUESTS += 1; | |
Trello.cards.get(card.id + "/actions", function (actions) { | |
var i, result, action, text; | |
var prev_estimate = 0; | |
console.log("Found " + actions.length + " Actions in Card '" + card.name + "' in List '" + | |
list.name + "'' in Board '" + board.name + "'."); | |
card.actions = actions; | |
for (i = 0; i < actions.length; i += 1) { | |
action = actions[i]; | |
action.card_name = card.name; | |
text = action.data && action.data.text; | |
if (ISO_DATETIME_OVERRIDE_RE.test(text)) { | |
result = ISO_DATETIME_OVERRIDE_RE.exec(text); | |
action.date = result[1]; | |
}; | |
}; | |
actions.sort(iso_date_comp); | |
for (i = 0; i < actions.length; i += 1) { | |
action = actions[i]; | |
text = action.data && action.data.text; | |
if (! board.oldest_action_date || board.oldest_action_date.localeCompare(action.date) > 0) { | |
board.oldest_action_date = action.date; | |
} | |
if (include_estimates) { | |
if (ESTIMATE_DAY_RE.test(text)) { | |
console.log("Estimate day '" + text + "' ..."); | |
result = ESTIMATE_DAY_RE.exec(text); | |
action.estimate = parseInt(result[1], 10); | |
if (result[2] != undefined) action.estimate += parseFloat(result[2], 10); | |
action.delta_estimate = action.estimate - prev_estimate | |
prev_estimate = action.estimate | |
} else if (ESTIMATE_HOUR_RE.test(text)) { | |
console.log("Estimate hour '" + text + "' ..."); | |
result = ESTIMATE_HOUR_RE.exec(text); | |
action.estimate = parseInt(result[1], 10) / HOURS_PER_DAY; | |
if (result[2] != undefined) action.estimate += parseFloat(result[2], 10) / HOURS_PER_DAY; | |
action.delta_estimate = action.estimate - prev_estimate | |
prev_estimate = action.estimate | |
} else if (WORK_DAY_RE.test(text)) { | |
console.log("Work day '" + text + "' ..."); | |
result = WORK_DAY_RE.exec(text); | |
action.work = parseInt(result[1], 10); | |
if (result[2] != undefined) action.work += parseFloat(result[2], 10); | |
} else if (WORK_HOUR_RE.test(text)) { | |
console.log("Work hour '" + text + "' ..."); | |
result = WORK_HOUR_RE.exec(text); | |
action.work = parseInt(result[1], 10) / HOURS_PER_DAY; | |
if (result[2] != undefined) action.work += parseFloat(result[2], 10) / HOURS_PER_DAY; | |
} | |
} | |
}; | |
OUTSTANDING_REQUESTS -= 1; | |
if (OUTSTANDING_REQUESTS == 0) { | |
display(); | |
} | |
}); | |
}; | |
function display() | |
{ | |
var i; | |
$("<div>") | |
.text("Showing " + BOARDS.length + " Boards:") | |
.appendTo($boards); | |
for (i = 0; i < BOARDS.length; i += 1) { | |
display_board(BOARDS[i]); | |
} | |
}; | |
function display_board(board) | |
{ | |
if (! board.start_date) { | |
board.start_date = board.oldest_action_date; | |
} | |
var data = raw_data_table(board); | |
var chart_data = chart_data_table(board, data); | |
// Add the start date row into the raw data table to display in the details table. | |
data.addRows([ [str2date(board.start_date), START_CARD, | |
0, 0, 0, | |
chart_data.getValue(0, 1), chart_data.getValue(0, 2), chart_data.getValue(0, 3)] ]); | |
data.sort([{ column: 0 }]); | |
var formatter = new google.visualization.NumberFormat({fractionDigits: 1}); | |
formatter.format(data, 2); | |
formatter.format(data, 3); | |
formatter.format(data, 4); | |
formatter.format(data, 5); | |
formatter.format(data, 6); | |
formatter.format(data, 7); | |
formatter.format(chart_data, 1); | |
formatter.format(chart_data, 2); | |
formatter.format(chart_data, 3); | |
$("<p>") | |
.addClass("board") | |
.text(board.name) | |
.appendTo($boards); | |
$("<p>") | |
.addClass("card") | |
.text(board.desc) | |
.appendTo($boards); | |
if (chart_data.getNumberOfRows() > 0) { | |
$("<div id=\"chart" + board.id + "\">") | |
.appendTo($boards); | |
var chart = new google.visualization.LineChart(document.getElementById("chart" + board.id)); | |
chart.draw(chart_data, { | |
vAxis: { title: "Days" }, | |
pointSize: 4 | |
}); | |
}; | |
if (false && chart_data.getNumberOfRows() > 0) { // Useful for debugging the chart above | |
$("<div>") | |
.text("Estimate Totals") | |
.appendTo($boards); | |
$("<div id=\"charttable" + board.id + "\">") | |
.appendTo($boards); | |
var table = new google.visualization.Table(document.getElementById("charttable" + board.id)); | |
table.draw(chart_data, { | |
}); | |
}; | |
if (data.getNumberOfRows() > 0) { | |
$("<a id=\"toggle" + board.id + "\" href=\"#\">") | |
.text("Toggle Estimation Details Table") | |
.appendTo($boards); | |
$("<div id=\"table" + board.id + "\" style=\"display: none\">") | |
.appendTo($boards); | |
$("#toggle" + board.id) | |
.click(function () { | |
$("#table" + board.id).toggle(); | |
}); | |
var table = new google.visualization.Table(document.getElementById("table" + board.id)); | |
table.draw(data, { | |
}); | |
}; | |
$("<p>") | |
.addClass("board") | |
.text(" ") | |
.appendTo($boards); | |
}; | |
function raw_data_table(board) | |
{ | |
var total_estimate = 0; | |
var total_work = 0; | |
var outstanding_work = 0; | |
var action_table = []; | |
var data = new google.visualization.DataTable(); | |
data.addColumn('date', 'When'); | |
data.addColumn('string', 'Task'); | |
data.addColumn('number', 'Estimate'); | |
data.addColumn('number', 'Delta Estimate'); | |
data.addColumn('number', 'Work Done'); | |
data.addColumn('number', 'Total Estimate'); | |
data.addColumn('number', 'Total Work Done'); | |
data.addColumn('number', 'Outstanding Work To Do'); | |
$.each(board.lists, function (ix, list) { | |
$.each(list.cards, function (ix, card) { | |
$.each(card.actions, function (ix, action) { | |
action_table.push(action); | |
}); | |
}); | |
}); | |
action_table.sort(iso_date_comp); | |
$.each(action_table, function (ix, action) { | |
if (action.estimate) { | |
total_estimate += action.delta_estimate; | |
outstanding_work += action.delta_estimate; | |
data.addRows([ [str2date(action.date), action.card_name, | |
action.estimate, action.delta_estimate, 0, | |
total_estimate, total_work, outstanding_work] ]); | |
} | |
if (action.work) { | |
total_work += action.work; | |
outstanding_work -= action.work; | |
data.addRows([ [str2date(action.date), action.card_name, | |
0, 0, action.work, | |
total_estimate, total_work, outstanding_work] ]); | |
} | |
}); | |
data.sort([{ column: 0 }]); | |
return data; | |
}; | |
function chart_data_table(board, data) | |
{ | |
var i, n; | |
var overall_estimate = 0; | |
var done = 0; | |
var to_do = 0; | |
var pre_start_view = new google.visualization.DataView(data); | |
var post_start_view = new google.visualization.DataView(data); | |
var chart_data = new google.visualization.DataTable(); | |
chart_data.addColumn('date', 'When'); | |
chart_data.addColumn('number', 'Overall Estimate (days)'); | |
chart_data.addColumn('number', 'Done (days)'); | |
chart_data.addColumn('number', 'To Do (days)'); | |
pre_start_view.setRows(pre_start_view.getFilteredRows([{ column: 0, maxValue: str2date(board.start_date) }])); | |
n = pre_start_view.getNumberOfRows(); | |
if (n > 0) { | |
overall_estimate = pre_start_view.getValue(n-1, 5); // Total Estimate | |
done = pre_start_view.getValue(n-1, 6); // Total Work Done | |
to_do = pre_start_view.getValue(n-1, 7); // Outstanding Work To Do | |
chart_data.addRows([ [str2date(board.start_date), overall_estimate, done, to_do] ]); | |
} else { | |
chart_data.addRows([ [str2date(board.start_date), 0, 0, 0] ]); | |
}; | |
post_start_view.hideRows(pre_start_view.getViewRows()); | |
n = post_start_view.getNumberOfRows(); | |
for (i = 0; i < n; i += 1) { | |
if (i === n-1 || post_start_view.getValue(i+1, 0) > post_start_view.getValue(i, 0)) { | |
overall_estimate = post_start_view.getValue(i, 5); // Total Estimate | |
done = post_start_view.getValue(i, 6); // Total Work Done | |
to_do = post_start_view.getValue(i, 7); // Outstanding Work To Do | |
chart_data.addRows([ [post_start_view.getValue(i, 0), overall_estimate, done, to_do] ]); | |
}; | |
}; | |
return chart_data; | |
}; | |
//]]> | |
</script> | |
</html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment