Skip to content

Instantly share code, notes, and snippets.

@gesellix
Forked from rtraschke/Trello_Burndown.html
Created July 28, 2018 21:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gesellix/c8693e1b2d4276c3492a55f65a664d7b to your computer and use it in GitHub Desktop.
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…
<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