January 10, 2012

Sprint Burndown Charts

This article shows how to draw sprint burndown charts using Dojo. Here is the JavaScript code:

// Some globals.
defaultTitleColor = "darkblue";
defaultXAxisTitle = "Days";
defaultYAxisTitle = "Remaining / Verified Tasks";
dojoInitialized = false;
chartArgs = new Array();
holidayDatesArray = new Array();

var themeClasses = new Object();

// Some utility functions.
$.getScript = function(url, callback, cache){
 $.ajax({
   type: "GET",
   url: url,
   success: callback,
   dataType: "script",
   cache: cache
 });
};

var addBusinessDays = function(d, n) {
 d = new Date(d.getTime());
 var day = d.getDay();
 d.setDate(d.getDate() + n + (day === 6 ? 2 : + !day) + (Math.floor((n - 1 + (day % 6 || 1)) / 5) * 2));
 return d;
}

var setDayValues = function (seriesIndex, days) {
 for (var i = 0; i < days; i++) {
  this.data.setValue(i, seriesIndex, "" + (i + 1));
 }
}

var datesEqual = function(a, b) {
   return (!(a > b || b > a));
}

var setDayLabels = function(startDate, days, dayLabels) {
 var daysSkiped = 0;
 for (var i = 0; i < days + 1; i++) {
  if (i == 0) {
   dayLabels.push({value: i + 1, text: ""});
   continue;
  }
  
  // We need to skip the weekends.
  var date = addBusinessDays(new Date(startDate), i - 1 + daysSkiped);
  
  // We need to skip the holidays.
  var isHoliday = false;
  while (holidayDatesArray != null && !isHoliday) {
   for (var j = 0; j < holidayDatesArray.length; j++) {
    if (datesEqual(date, holidayDatesArray[j])) {
     isHoliday = true;
     break;
    }
   }
   
   if (isHoliday) {
    date = addBusinessDays(date, 1);
    daysSkiped++;
    isHoliday = false;
   } else {
    break;
   }
  }

  var label = date.toDateString();
  label = label.substring(label.indexOf(" ") + 1, label.lastIndexOf(" "));
  dayLabels.push({value: i + 1, text: label});
 }
}

// A function to initialize the chart.
var initChart = function(chart, tensionValue, animationDuration, startDate, days, useDayLabels, xAxisTitle, yAxisTitle, theme) {
 var dayLabels = [];
 var xAxisLabelsRotationValue = 0;
 
 // Set the x axis labels. We do this only if the client tell us to do so.
 // Otherwise the default values will be used, which is 1, 2, 3, ...
 if (useDayLabels) {
  xAxisLabelsRotationValue = -15;
  setDayLabels(startDate, days, dayLabels);
 }
 
 // Set the chart theme.
 chart.setTheme(themeClasses[theme.toLowerCase()]);

 // Set the chart plots and axies.
 chart.addPlot("default", {
  type: "Lines", 
  animate: { 
   duration: animationDuration, 
   easing: dojox.fx.easing.linear}, 
  markers: true, 
  tension: tensionValue}
 );
 
 chart.addAxis("x", {
  minorTicks: true, 
  minorTickStep: 1, 
  stroke: "gray", 
  font: "normal normal normal 8pt Tahoma", 
  title: xAxisTitle, 
  titleOrientation: "away", 
  titleFontColor: "gray", 
  labels: dayLabels, 
  maxLabelSize: 80, 
  rotation: xAxisLabelsRotationValue}
 );
 
 chart.addAxis("y", {
  vertical: true, 
  includeZero: true, 
  minorTicks: false, 
  stroke: "gray", 
  font: "normal normal normal 8pt Tahoma", 
  title: yAxisTitle, 
  titleFontColor: "gray"}
 );
}

// Sets the values for ideal burndown.
var getIdealBurndownData = function(days, tasksRemainingInitValue) {
 var values = new Array();
 for (var i = 0; i < days + 1; i++) {
  values.push(tasksRemainingInitValue - (tasksRemainingInitValue / days) * i);
 }
 return values;
}

// Sets the values for a data series.
var plotChartData = function(chart, args) {
 chart.addSeries("Ideal Breakdown", getIdealBurndownData(args.days, args.tasksRemaining[0]));
 chart.addSeries("Remaining Tasks", args.tasksRemaining);
 chart.addSeries("Verified Tasks", args.tasksVerified);
}

// Initializes the holiday dates array from a string array.
var stringsToDates = function(strings, dates) {
 for (var i = 0; i < strings.length; i++) {
  dates.push(new Date(strings[i]));
 }
}

// Draws the burndown chart.
var drawBurndownInternal = function(args) {
 // Initialize the variables.
 var titleColor = args.titleColor;
 if (!titleColor) titleColor = defaultTitleColor;
 
 var chartName = "burndownChart";
 if (args.chartName) chartName = args.chartName;
 
 var legendName = "burndownChartLegend";
 if (args.legendName) legendName = args.legendName;
 
 var xAxisTitle = args.xAxisTitle;
 if (!xAxisTitle) xAxisTitle = defaultXAxisTitle;
 
 var yAxisTitle = args.yAxisTitle;
 if (!yAxisTitle) yAxisTitle = defaultYAxisTitle;

 var tensionValue = "S";
 if (args.curvedLines == false) tensionValue = "";
 
 var animationDuration = 500;
 if (args.animate == false) animationDuration = 0;
 
 var useDayLabels = true;
 if (!isNaN(args.useDayLabels)) useDayLabels = args.useDayLabels;
 
 if (args.holidays != null && args.holidays.length > 0) {
  stringsToDates(args.holidays, holidayDatesArray);
 }
 
 var theme = "WatersEdge";
 if (args.theme) theme = args.theme;

 // Create the chart object.
 var burndownChart = new dojox.charting.Chart2D(chartName, {
  title: args.title,
  titlePos: "top",
  titleGap: 25,
  titleFont: "normal normal bold 12pt Arial",
  titleFontColor: titleColor});

 initChart(burndownChart, tensionValue, animationDuration, args.startDate, args.days, useDayLabels, xAxisTitle, yAxisTitle, theme);
 plotChartData(burndownChart, args);

 // Set tooltips, magnification, etc...
 new dojox.charting.action2d.Magnify(burndownChart, "default", {scale: 2});
 //new dojox.charting.action2d.Tooltip(burndownChart, "default");
 //new dojox.charting.action2d.Highlight(burndownChart, "default");
 
 // Render the chart.
 burndownChart.render();

 // Create the legend.
 var legend = new dojox.charting.widget.Legend({
   chart: burndownChart, 
   horizontal: false, 
   style: "font-family:arial;color:black;font-size:10px;"}, 
  legendName);
}

// The function gets called after the Dojo is initialized.
var onDojoInitialized = function() {
 dojoInitialized = true;
 
 themeClasses["wetland"] = dojox.charting.themes.Wetland;
 themeClasses["tufte"] = dojox.charting.themes.Tufte;
 themeClasses["shrooms"] = dojox.charting.themes.Shrooms;
 themeClasses["watersedge"] = dojox.charting.themes.WatersEdge;
 
 for (var i = 0; i < chartArgs.length; i++) {
  drawBurndownInternal(chartArgs[i]);
 }
}

// The clients call this function to draw the burndown charts.
var drawBurndown = function(args) {
 if (!dojoInitialized) {
  $.getScript('http://ajax.googleapis.com/ajax/libs/dojo/1.6.0/dojo/dojo.xd.js', function() {
   dojo.require("dojo.fx");
   dojo.require("dojox.fx.easing");
   dojo.require("dojox.charting.Chart2D");
   dojo.require("dojox.charting.action2d.Magnify");
   dojo.require("dojox.charting.action2d.Tooltip");
   dojo.require("dojox.charting.action2d.Highlight");
   dojo.require("dojox.charting.widget.Legend");
   dojo.require("dojox.charting.themes.WatersEdge");
   dojo.require("dojox.charting.themes.Wetland");
   dojo.require("dojox.charting.themes.Tufte");
   dojo.require("dojox.charting.themes.Shrooms");
   
   // This is a HACK!
   // When I tried 2 charts on the same page, only one chart was rendered.
   // I guess this is because of the second request is not going into onDojoInitialized function.
   // So as a hack, we cache the arguments and then after the Dojo is initialized we start drawing the charts.
   chartArgs.push(args);
   
   dojo.ready(function() { onDojoInitialized(); });
  });
 } else {
  drawBurndownInternal(args);
 }
};

Here is the HTML code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<body style="font-family: Arial;border: 0 none;">
 <script src="http://code.jquery.com/jquery-latest.js"></script>
 <script type="text/javascript" src="burndownChart.js"></script>
 <table border="0"><tr valign="top">
  <td><div id="burndownChart" style="width: 800px; height: 400px;"></div></td>
  <td><div style="width: 100px; margin-top:100px; border: 1px solid #C8C8C8; padding: 5px; background-color: rgba(255, 255, 221, 0.8);"><div id="burndownChartLegend" ></div>
  </div></td>
 </tr></table>

 <script type="text/javascript">
  drawBurndown({
   title: "Linking 2.0 Sprint 9 Burdown Chart",
   startDate: "2011/10/24",
   days: 15,
   tasksRemaining: [50,49,49,53,57,54,33,31,28,28,27,24,23,23,23,16],
   tasksVerified: [0,1,1,5,6,6,7,13,16,16,17,20,21,21,21,30]
  });
 </script> 
</body>

</html>

No comments: