diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 405: Image reports revamp. Includes new filters in the page as well as the graph for test results. Rev...

Message ID 20130611122218.16829.59346.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Stevan Radakovic June 11, 2013, 12:22 p.m. UTC
Merge authors:
  Stevan Radaković (stevanr)
Related merge proposals:
  https://code.launchpad.net/~stevanr/lava-dashboard/image-reports-revamp/+merge/166758
  proposed by: Stevan Radaković (stevanr)
------------------------------------------------------------
revno: 405 [merge]
committer: Stevan Radakovic <stevan.radakovic@linaro.org>
branch nick: trunk
timestamp: Tue 2013-06-11 14:21:11 +0200
message:
  Image reports revamp. Includes new filters in the page as well as the graph for test results. Reveiewed by terceiro.
added:
  dashboard_app/static/dashboard_app/js/jquery.flot.dashes.min.js
  dashboard_app/static/dashboard_app/js/jquery.tooltip.min.js
  dashboard_app/static/dashboard_app/js/jstorage.min.js
modified:
  dashboard_app/static/dashboard_app/css/image-report.css
  dashboard_app/static/dashboard_app/js/image-report.js
  dashboard_app/templates/dashboard_app/image-report.html
  dashboard_app/views/images.py


--
lp:lava-dashboard
https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk

You are subscribed to branch lp:lava-dashboard.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk/+edit-subscription
diff mbox

Patch

=== modified file 'dashboard_app/static/dashboard_app/css/image-report.css'
--- dashboard_app/static/dashboard_app/css/image-report.css	2012-07-13 04:51:22 +0000
+++ dashboard_app/static/dashboard_app/css/image-report.css	2013-06-10 15:33:53 +0000
@@ -9,7 +9,7 @@ 
     text-align: left;
 }
 .inner-table td, .inner-table th {
-    min-width: 25ex;
+    min-width: 90px;
     padding: 3px 4px;
     border: thin solid black;
 }
@@ -43,3 +43,47 @@ 
 #go-to-bug-dialog {
     text-align: center;
 }
+#inner-container {
+    width: 80%;
+    height:250px;
+    margin:0 auto;
+    margin-left: 10px;
+    float: left;
+}
+#legend-container {
+    float: left;
+    margin-left: 20px;
+}
+#build_numbers_filter {
+    margin: 5px 0 0 20px;
+}
+#outer-container {
+    overflow: auto;
+}
+#filters {
+    border: 1px solid #000000;
+    clear: both;
+    margin: 20px 2px 20px 0px;
+}
+#tests_filter, #graph_type_filter, #target_goal_filter {
+    margin: 5px 0 10px 20px;
+}
+#filter_headline {
+    font-weight: bold;
+    font-size: 16px;
+    margin: 5px 0 0 10px;
+}
+#test_headline, #build_number_headline, #graph_type_headline, #target_goal_headline {
+    width: 120px;
+    float: left;
+}
+#tooltip {
+    position: absolute;
+    z-index: 3000;
+    border: 1px solid #111;
+    background-color: #eee;
+    color: #000;
+    padding: 5px;
+    opacity: 0.85;
+}
+#tooltip h3, #tooltip div { margin: 0; }

=== modified file 'dashboard_app/static/dashboard_app/js/image-report.js'
--- dashboard_app/static/dashboard_app/js/image-report.js	2012-08-24 00:24:42 +0000
+++ dashboard_app/static/dashboard_app/js/image-report.js	2013-06-10 15:33:53 +0000
@@ -22,6 +22,260 @@ 
         resultRow.css('height', Math.max(nameRowHeight, resultRowHeight));
     }
 }
+
+function update_filters(column_data, test_run_names) {
+    for (iter in column_data) {
+	build_number = column_data[iter]["number"].split('.')[0];
+	$("#build_number_start").append($('<option>', {
+	    value: build_number,
+	    text: build_number
+	}));
+	$("#build_number_end").append($('<option>', {
+	    value: build_number,
+	    text: build_number
+	}));
+    }
+    $("#build_number_end option:last").attr("selected", true);
+
+    for (iter in test_run_names) {
+	selected = false;
+	if (column_data[column_data.length-1]["test_runs"][test_run_names[iter]]) {
+	    selected = true;
+	}
+	$("#test_select").append($('<option>', {
+	    value: test_run_names[iter],
+	    text: test_run_names[iter],
+	    selected: selected
+	}));
+    }
+
+    // Use jStorage to load the filter values from browser.
+    load_filters();
+}
+
+function update_table(column_data, table_data, test_run_names) {
+
+    if ($("#test_select").val() == null) {
+	alert("Please select at least one test.");
+	return false;
+    }
+
+    if ($("#build_number_start").val() > $("#build_number_end").val()) {
+	alert("End build number must be greater then the start build number.");
+	return false;
+    }
+
+    // Create row headlines.
+    test_name_rows = "<tr><td>Date</td></tr>";
+    for (iter in test_run_names) {
+	if ($("#test_select").val().indexOf(test_run_names[iter]) >= 0) {
+	    test_name = test_run_names[iter];
+	    if (test_name.length > 20) {
+		test_name = test_name.substring(0,20) + "...";
+	    }
+	    test_name_rows += "<tr><td tooltip='" + test_run_names[iter] + "'>" + test_name + "</td></tr>";
+	}
+    }
+    $("#test-run-names tbody").html(test_name_rows);
+
+    // Create column headlines.
+    result_table_head = "<tr>";
+    for (iter in column_data) {
+	build_number = column_data[iter]["number"].split('.')[0];
+
+	if (build_number <= $("#build_number_end").val() && build_number >= $("#build_number_start").val()) {
+	    link = '<a href="' + column_data[iter]["link"] + '">' + build_number.split(' ')[0] + '</a>';
+	    result_table_head += "<th>" + link + "</th>";
+	}
+    }
+    result_table_head += "</tr>";
+    $("#results-table thead").html(result_table_head);
+
+    // Create table body
+    result_table_body = "<tr>";
+    for (iter in column_data) {
+	build_number = column_data[iter]["number"].split('.')[0];
+	build_date = column_data[iter]["date"].split('.')[0];
+
+	if (build_number <= $("#build_number_end").val() && build_number >= $("#build_number_start").val()) {
+	    result_table_body += "<td>" + build_date.split(' ')[0] + "</td>";
+	}
+
+    }
+    result_table_body += "</tr>";
+
+    for (cnt in test_run_names) {
+	test = test_run_names[cnt];
+	if ($("#test_select").val().indexOf(test) >= 0) {
+	    result_table_body += "<tr>";
+	    row = table_data[test];
+
+	    for (iter in row) {
+		build_number = column_data[iter]["number"].split('.')[0];
+		if (build_number <= $("#build_number_end").val() && build_number >= $("#build_number_start").val()) {
+		    result_table_body += '<td class="' + row[iter]["cls"] + '" data-uuid="' + row[iter]["uuid"] + '">';
+		    if (row[iter]["uuid"]) {
+			result_table_body += '<a href="' + row[iter]["link"] + '">' + row[iter]["passes"] + '/' + row[iter]["total"] + '</a>';
+			result_table_body += '<span class="bug-links">';
+			for (bug_id in row[iter]["bug_ids"]) {
+			    bug = row[iter]["bug_ids"];
+			    result_table_body += '<a class="bug-link" href="https://bugs.launchpad.net/bugs/' + bug[bug_id] + '" data-bug-id="' + bug[bug_id] + '">[' + bug[bug_id] + ']</a>';
+			}
+			result_table_body += '<a href="#" class="add-bug-link">[+]</a>';
+			result_table_body += '</span>';
+
+		    } else {
+			result_table_body += "&mdash;";
+		    }
+		    result_table_body += "</td>";
+		}
+	    }
+	    result_table_body += "</tr>";
+	}
+    }
+
+    $("#results-table tbody").html(result_table_body);
+    $("#scroller").scrollLeft($("#scroller")[0].scrollWidth);
+
+    // Use jStorage to save filter values to the browser.
+    store_filters();
+    update_plot(column_data, table_data, test_run_names);
+    update_tooltips();
+}
+
+function update_tooltips() {
+    // Update tooltips on the remaining td's for the test names.
+    $("td", "#test-run-names").each(function () {
+	if ($(this).attr('tooltip')) {
+	    $(this).tooltip({
+		bodyHandler: function() {
+		    return $(this).attr('tooltip');
+		}
+	    });
+	}
+    });
+}
+
+function store_filters() {
+    // Use jStorage to save filter values to the browser.
+
+    $.jStorage.set("target_goal", $("#target_goal").val());
+    $.jStorage.set("build_number_start", $("#build_number_start").val());
+    $.jStorage.set("build_number_end", $("#build_number_end").val());
+    $.jStorage.set("test_select", $("#test_select").val());
+    $.jStorage.set("graph_type", $('input:radio[name=graph_type]:checked').val());
+}
+
+function load_filters() {
+    // Use jStorage to load the filter values from browser.
+
+    if ($.jStorage.get("target_goal")) {
+	$("#target_goal").val($.jStorage.get("target_goal"));
+    }
+    if ($.jStorage.get("build_number_start")) {
+	$("#build_number_start").val($.jStorage.get("build_number_start"));
+    }
+    if ($.jStorage.get("build_number_end")) {
+	$("#build_number_end").val($.jStorage.get("build_number_end"));
+    }
+    if ($.jStorage.get("test_select")) {
+	$("#test_select").val($.jStorage.get("test_select"));
+    }
+    if ($.jStorage.get("graph_type")) {
+	if ($.jStorage.get("graph_type") == "number") {
+	    $('input:radio[name=graph_type][value="number"]').attr("checked", true);
+	} else {
+	    $('input:radio[name=graph_type][value="percentage"]').attr("checked", true);
+	}
+    }
+}
+
+function update_plot(column_data, table_data, test_run_names) {
+
+    // Get the plot data.
+
+    data = [];
+    for (test in table_data) {
+
+	if ($("#test_select").val().indexOf(test) >= 0) {
+	    row_data = [];
+
+	    row = table_data[test];
+	    for (iter in row) {
+		build_number = column_data[iter]["number"].split('.')[0];
+		if (build_number <= $("#build_number_end").val() && build_number >= $("#build_number_start").val()) {
+		    if (row[iter]["cls"]) {
+			if ($('input:radio[name=graph_type]:checked').val() == "number") {
+			    row_data.push([iter, row[iter]["passes"]]); 
+			} else {
+			    if (isNaN(row[iter]["passes"]/row[iter]["total"])) {
+				row_data.push([iter, 0]);
+			    } else {
+				row_data.push([iter, 100*row[iter]["passes"]/row[iter]["total"]]);
+			    }
+			}
+		    }
+		}
+	    }
+	    data.push({label: test, data: row_data});
+	}
+    }
+
+    // Add target goal dashed line to the plot.
+    if ($("#target_goal").val()) {
+	row_data = [];
+	row = table_data[test_run_names[0]];
+	for (iter in row) {
+	    build_number = column_data[iter]["number"].split('.')[0];
+	    if (build_number <= $("#build_number_end").val() && build_number >= $("#build_number_start").val()) {
+		row_data.push([iter, $("#target_goal").val()]);
+	    }
+	}
+	data.push({data: row_data, dashes: {show: true}, lines: {show: false}, color: "#000000"});
+    }
+
+    // Get all build numbers to be used as tick labels.
+    build_numbers = [];
+    for (test in table_data) {
+	row = table_data[test];
+	for (iter in row) {
+	    build_numbers.push(column_data[iter]["number"].split(' ')[0]);
+	}
+	// Each test has the same number of build numbers.
+	break;
+    }
+
+    var options = {
+	series: {
+	    lines: { show: true },
+	    points: { show: false }
+	},
+	legend: {
+	    show: true,
+	    position: "ne",
+	    margin: 3,
+	    container: "#legend-container",
+	},
+	xaxis: {
+	    tickDecimals: 0,
+	    tickFormatter: function (val, axis) {
+		return build_numbers[val];
+	    },
+	},
+	yaxis: {
+	    tickDecimals: 0,
+	},
+    };
+
+    if ($('input:radio[name=graph_type]:checked').val() == "percentage") {
+	options["yaxis"]["max"] = 100;
+	options["yaxis"]["min"] = 0;
+    }
+
+
+    $.plot($("#outer-container #inner-container"), data, options); 
+}
+
 $(window).ready(
     function () {
         // Hook up the event and run resize ASAP (looks jumpy in FF if you
@@ -139,3 +393,5 @@ 
 // chromium if you don't do this).
 $(window).load(_resize);
 $(window).load(_fixRowHeights);
+$(window).load(function() {update_filters(columns, test_names);});
+$(window).load(function() {update_table(columns, chart_data, test_names);});

=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.dashes.min.js'
--- dashboard_app/static/dashboard_app/js/jquery.flot.dashes.min.js	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/dashboard_app/js/jquery.flot.dashes.min.js	2013-06-10 13:43:14 +0000
@@ -0,0 +1,29 @@ 
+/*
+ * jQuery.flot.dashes
+ * 
+ * options = {
+ *   series: {
+ *     dashes: {
+ *       
+ *       // show
+ *       // default: false
+ *       // Whether to show dashes for the series.
+ *       show: <boolean>,
+ *       
+ *       // lineWidth
+ *       // default: 2
+ *       // The width of the dashed line in pixels.
+ *       lineWidth: <number>,
+ *       
+ *       // dashLength
+ *       // default: 10
+ *       // Controls the length of the individual dashes and the amount of 
+ *       // space between them.
+ *       // If this is a number, the dashes and spaces will have that length.
+ *       // If this is an array, it is read as [ dashLength, spaceLength ]
+ *       dashLength: <number> or <array[2]>
+ *     }
+ *   }
+ * }
+ */
+(function($){function init(plot){plot.hooks.processDatapoints.push(function(plot,series,datapoints){if(!series.dashes.show)return;plot.hooks.draw.push(function(plot,ctx){var plotOffset=plot.getPlotOffset(),axisx=series.xaxis,axisy=series.yaxis;function plotDashes(xoffset,yoffset){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null,dashRemainder=0,dashOn=true,dashOnLength,dashOffLength;if(series.dashes.dashLength[0]){dashOnLength=series.dashes.dashLength[0];if(series.dashes.dashLength[1]){dashOffLength=series.dashes.dashLength[1]}else{dashOffLength=dashOnLength}}else{dashOffLength=dashOnLength=series.dashes.dashLength}ctx.beginPath();for(var i=ps;i<points.length;i+=ps){var x1=points[i-ps],y1=points[i-ps+1],x2=points[i],y2=points[i+1];if(x1==null||x2==null)continue;if(y1<=y2&&y1<axisy.min){if(y2<axisy.min)continue;x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2<axisy.min){if(y1<axisy.min)continue;x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1<axisx.min){if(x2<axisx.min)continue;y1=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.min}else if(x2<=x1&&x2<axisx.min){if(x1<axisx.min)continue;y2=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.min}if(x1>=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy){ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset)}var ax1=axisx.p2c(x1)+xoffset,ay1=axisy.p2c(y1)+yoffset,ax2=axisx.p2c(x2)+xoffset,ay2=axisy.p2c(y2)+yoffset,dashOffset;function lineSegmentOffset(segmentLength){var c=Math.sqrt(Math.pow(ax2-ax1,2)+Math.pow(ay2-ay1,2));if(c<=segmentLength){return{deltaX:ax2-ax1,deltaY:ay2-ay1,distance:c,remainder:segmentLength-c}}else{var xsign=ax2>ax1?1:-1,ysign=ay2>ay1?1:-1;return{deltaX:xsign*Math.sqrt(Math.pow(segmentLength,2)/(1+Math.pow((ay2-ay1)/(ax2-ax1),2))),deltaY:ysign*Math.sqrt(Math.pow(segmentLength,2)-Math.pow(segmentLength,2)/(1+Math.pow((ay2-ay1)/(ax2-ax1),2))),distance:segmentLength,remainder:0}}}do{dashOffset=lineSegmentOffset(dashRemainder>0?dashRemainder:dashOn?dashOnLength:dashOffLength);if(dashOffset.deltaX!=0||dashOffset.deltaY!=0){if(dashOn){ctx.lineTo(ax1+dashOffset.deltaX,ay1+dashOffset.deltaY)}else{ctx.moveTo(ax1+dashOffset.deltaX,ay1+dashOffset.deltaY)}}dashOn=!dashOn;dashRemainder=dashOffset.remainder;ax1+=dashOffset.deltaX;ay1+=dashOffset.deltaY}while(dashOffset.distance>0);prevx=x2;prevy=y2}ctx.stroke()}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin='round';var lw=series.dashes.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotDashes(Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2));ctx.lineWidth=sw/2;plotDashes(Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4))}ctx.lineWidth=lw;ctx.strokeStyle=series.color;if(lw>0){plotDashes(0,0)}ctx.restore()})})}$.plot.plugins.push({init:init,options:{series:{dashes:{show:false,lineWidth:2,dashLength:10}}},name:'dashes',version:'0.1'})})(jQuery)

=== added file 'dashboard_app/static/dashboard_app/js/jquery.tooltip.min.js'
--- dashboard_app/static/dashboard_app/js/jquery.tooltip.min.js	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/dashboard_app/js/jquery.tooltip.min.js	2013-06-10 15:33:53 +0000
@@ -0,0 +1,19 @@ 
+/*
+ * jQuery Tooltip plugin 1.3
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/
+ * http://docs.jquery.com/Plugins/Tooltip
+ *
+ * Copyright (c) 2006 - 2008 Jörn Zaefferer
+ *
+ * $Id: jquery.tooltip.js 5741 2008-06-21 15:22:16Z joern.zaefferer $
+ * 
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ */;(function($){var helper={},current,title,tID,IE=$.browser.msie&&/MSIE\s(5\.5|6\.)/.test(navigator.userAgent),track=false;$.tooltip={blocked:false,defaults:{delay:200,fade:false,showURL:true,extraClass:"",top:15,left:15,id:"tooltip"},block:function(){$.tooltip.blocked=!$.tooltip.blocked;}};$.fn.extend({tooltip:function(settings){settings=$.extend({},$.tooltip.defaults,settings);createHelper(settings);return this.each(function(){$.data(this,"tooltip",settings);this.tOpacity=helper.parent.css("opacity");this.tooltipText=this.title;$(this).removeAttr("title");this.alt="";}).mouseover(save).mouseout(hide).click(hide);},fixPNG:IE?function(){return this.each(function(){var image=$(this).css('backgroundImage');if(image.match(/^url\(["']?(.*\.png)["']?\)$/i)){image=RegExp.$1;$(this).css({'backgroundImage':'none','filter':"progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='"+image+"')"}).each(function(){var position=$(this).css('position');if(position!='absolute'&&position!='relative')$(this).css('position','relative');});}});}:function(){return this;},unfixPNG:IE?function(){return this.each(function(){$(this).css({'filter':'',backgroundImage:''});});}:function(){return this;},hideWhenEmpty:function(){return this.each(function(){$(this)[$(this).html()?"show":"hide"]();});},url:function(){return this.attr('href')||this.attr('src');}});function createHelper(settings){if(helper.parent)return;helper.parent=$('<div id="'+settings.id+'"><h3></h3><div class="body"></div><div class="url"></div></div>').appendTo(document.body).hide();if($.fn.bgiframe)helper.parent.bgiframe();helper.title=$('h3',helper.parent);helper.body=$('div.body',helper.parent);helper.url=$('div.url',helper.parent);}function settings(element){return $.data(element,"tooltip");}function handle(event){if(settings(this).delay)tID=setTimeout(show,settings(this).delay);else
+show();track=!!settings(this).track;$(document.body).bind('mousemove',update);update(event);}function save(){if($.tooltip.blocked||this==current||(!this.tooltipText&&!settings(this).bodyHandler))return;current=this;title=this.tooltipText;if(settings(this).bodyHandler){helper.title.hide();var bodyContent=settings(this).bodyHandler.call(this);if(bodyContent.nodeType||bodyContent.jquery){helper.body.empty().append(bodyContent)}else{helper.body.html(bodyContent);}helper.body.show();}else if(settings(this).showBody){var parts=title.split(settings(this).showBody);helper.title.html(parts.shift()).show();helper.body.empty();for(var i=0,part;(part=parts[i]);i++){if(i>0)helper.body.append("<br/>");helper.body.append(part);}helper.body.hideWhenEmpty();}else{helper.title.html(title).show();helper.body.hide();}if(settings(this).showURL&&$(this).url())helper.url.html($(this).url().replace('http://','')).show();else
+helper.url.hide();helper.parent.addClass(settings(this).extraClass);if(settings(this).fixPNG)helper.parent.fixPNG();handle.apply(this,arguments);}function show(){tID=null;if((!IE||!$.fn.bgiframe)&&settings(current).fade){if(helper.parent.is(":animated"))helper.parent.stop().show().fadeTo(settings(current).fade,current.tOpacity);else
+helper.parent.is(':visible')?helper.parent.fadeTo(settings(current).fade,current.tOpacity):helper.parent.fadeIn(settings(current).fade);}else{helper.parent.show();}update();}function update(event){if($.tooltip.blocked)return;if(event&&event.target.tagName=="OPTION"){return;}if(!track&&helper.parent.is(":visible")){$(document.body).unbind('mousemove',update)}if(current==null){$(document.body).unbind('mousemove',update);return;}helper.parent.removeClass("viewport-right").removeClass("viewport-bottom");var left=helper.parent[0].offsetLeft;var top=helper.parent[0].offsetTop;if(event){left=event.pageX+settings(current).left;top=event.pageY+settings(current).top;var right='auto';if(settings(current).positionLeft){right=$(window).width()-left;left='auto';}helper.parent.css({left:left,right:right,top:top});}var v=viewport(),h=helper.parent[0];if(v.x+v.cx<h.offsetLeft+h.offsetWidth){left-=h.offsetWidth+20+settings(current).left;helper.parent.css({left:left+'px'}).addClass("viewport-right");}if(v.y+v.cy<h.offsetTop+h.offsetHeight){top-=h.offsetHeight+20+settings(current).top;helper.parent.css({top:top+'px'}).addClass("viewport-bottom");}}function viewport(){return{x:$(window).scrollLeft(),y:$(window).scrollTop(),cx:$(window).width(),cy:$(window).height()};}function hide(event){if($.tooltip.blocked)return;if(tID)clearTimeout(tID);current=null;var tsettings=settings(this);function complete(){helper.parent.removeClass(tsettings.extraClass).hide().css("opacity","");}if((!IE||!$.fn.bgiframe)&&tsettings.fade){if(helper.parent.is(':animated'))helper.parent.stop().fadeTo(tsettings.fade,0,complete);else
+helper.parent.stop().fadeOut(tsettings.fade,complete);}else
+complete();if(settings(this).fixPNG)helper.parent.unfixPNG();}})(jQuery);
\ No newline at end of file

=== added file 'dashboard_app/static/dashboard_app/js/jstorage.min.js'
--- dashboard_app/static/dashboard_app/js/jstorage.min.js	1970-01-01 00:00:00 +0000
+++ dashboard_app/static/dashboard_app/js/jstorage.min.js	2013-06-10 12:19:28 +0000
@@ -0,0 +1,27 @@ 
+/*
+ * ----------------------------- JSTORAGE -------------------------------------
+ * Simple local storage wrapper to save data on the browser side, supporting
+ * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
+ *
+ * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
+ * Project homepage: www.jstorage.info
+ *
+ * Licensed under MIT-style license:
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+(function(){var JSTORAGE_VERSION="0.4.3",$=window.jQuery||window.$||(window.$={}),JSON={parse:window.JSON&&(window.JSON.parse||window.JSON.decode)||String.prototype.evalJSON&&function(str){return String(str).evalJSON()}||$.parseJSON||$.evalJSON,stringify:Object.toJSON||window.JSON&&(window.JSON.stringify||window.JSON.encode)||$.toJSON};if(!JSON.parse||!JSON.stringify){throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page")}var _storage={__jstorage_meta:{CRC32:{}}},_storage_service={jStorage:"{}"},_storage_elm=null,_storage_size=0,_backend=false,_observers={},_observer_timeout=false,_observer_update=0,_pubsub_observers={},_pubsub_last=+new Date(),_ttl_timeout,_XMLService={isXML:function(elm){var documentElement=(elm?elm.ownerDocument||elm:0).documentElement;return documentElement?documentElement.nodeName!=="HTML":false},encode:function(xmlNode){if(!this.isXML(xmlNode)){return false}try{return new XMLSerializer().serializeToString(xmlNode)}catch(E1){try{return xmlNode.xml}catch(E2){}}return false},decode:function(xmlString){var dom_parser=("DOMParser"in window&&(new DOMParser()).parseFromString)||(window.ActiveXObject&&function(_xmlString){var xml_doc=new ActiveXObject('Microsoft.XMLDOM');xml_doc.async='false';xml_doc.loadXML(_xmlString);return xml_doc}),resultXML;if(!dom_parser){return false}resultXML=dom_parser.call("DOMParser"in window&&(new DOMParser())||window,xmlString,'text/xml');return this.isXML(resultXML)?resultXML:false}};function _init(){var localStorageReallyWorks=false;if("localStorage"in window){try{window.localStorage.setItem('_tmptest','tmpval');localStorageReallyWorks=true;window.localStorage.removeItem('_tmptest')}catch(BogusQuotaExceededErrorOnIos5){}}if(localStorageReallyWorks){try{if(window.localStorage){_storage_service=window.localStorage;_backend="localStorage";_observer_update=_storage_service.jStorage_update}}catch(E3){}}else if("globalStorage"in window){try{if(window.globalStorage){if(window.location.hostname=='localhost'){_storage_service=window.globalStorage['localhost.localdomain']}else{_storage_service=window.globalStorage[window.location.hostname]}_backend="globalStorage";_observer_update=_storage_service.jStorage_update}}catch(E4){}}else{_storage_elm=document.createElement('link');if(_storage_elm.addBehavior){_storage_elm.style.behavior='url(#default#userData)';document.getElementsByTagName('head')[0].appendChild(_storage_elm);try{_storage_elm.load("jStorage")}catch(E){_storage_elm.setAttribute("jStorage","{}");_storage_elm.save("jStorage");_storage_elm.load("jStorage")}var data="{}";try{data=_storage_elm.getAttribute("jStorage")}catch(E5){}try{_observer_update=_storage_elm.getAttribute("jStorage_update")}catch(E6){}_storage_service.jStorage=data;_backend="userDataBehavior"}else{_storage_elm=null;return}}_load_storage();_handleTTL();_setupObserver();_handlePubSub();if("addEventListener"in window){window.addEventListener("pageshow",function(event){if(event.persisted){_storageObserver()}},false)}}function _reloadData(){var data="{}";if(_backend=="userDataBehavior"){_storage_elm.load("jStorage");try{data=_storage_elm.getAttribute("jStorage")}catch(E5){}try{_observer_update=_storage_elm.getAttribute("jStorage_update")}catch(E6){}_storage_service.jStorage=data}_load_storage();_handleTTL();_handlePubSub()}function _setupObserver(){if(_backend=="localStorage"||_backend=="globalStorage"){if("addEventListener"in window){window.addEventListener("storage",_storageObserver,false)}else{document.attachEvent("onstorage",_storageObserver)}}else if(_backend=="userDataBehavior"){setInterval(_storageObserver,1000)}}function _storageObserver(){var updateTime;clearTimeout(_observer_timeout);_observer_timeout=setTimeout(function(){if(_backend=="localStorage"||_backend=="globalStorage"){updateTime=_storage_service.jStorage_update}else if(_backend=="userDataBehavior"){_storage_elm.load("jStorage");try{updateTime=_storage_elm.getAttribute("jStorage_update")}catch(E5){}}if(updateTime&&updateTime!=_observer_update){_observer_update=updateTime;_checkUpdatedKeys()}},25)}function _checkUpdatedKeys(){var oldCrc32List=JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),newCrc32List;_reloadData();newCrc32List=JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));var key,updated=[],removed=[];for(key in oldCrc32List){if(oldCrc32List.hasOwnProperty(key)){if(!newCrc32List[key]){removed.push(key);continue}if(oldCrc32List[key]!=newCrc32List[key]&&String(oldCrc32List[key]).substr(0,2)=="2."){updated.push(key)}}}for(key in newCrc32List){if(newCrc32List.hasOwnProperty(key)){if(!oldCrc32List[key]){updated.push(key)}}}_fireObservers(updated,"updated");_fireObservers(removed,"deleted")}function _fireObservers(keys,action){keys=[].concat(keys||[]);if(action=="flushed"){keys=[];for(var key in _observers){if(_observers.hasOwnProperty(key)){keys.push(key)}}action="deleted"}for(var i=0,len=keys.length;i<len;i++){if(_observers[keys[i]]){for(var j=0,jlen=_observers[keys[i]].length;j<jlen;j++){_observers[keys[i]][j](keys[i],action)}}if(_observers["*"]){for(var j=0,jlen=_observers["*"].length;j<jlen;j++){_observers["*"][j](keys[i],action)}}}}function _publishChange(){var updateTime=(+new Date()).toString();if(_backend=="localStorage"||_backend=="globalStorage"){_storage_service.jStorage_update=updateTime}else if(_backend=="userDataBehavior"){_storage_elm.setAttribute("jStorage_update",updateTime);_storage_elm.save("jStorage")}_storageObserver()}function _load_storage(){if(_storage_service.jStorage){try{_storage=JSON.parse(String(_storage_service.jStorage))}catch(E6){_storage_service.jStorage="{}"}}else{_storage_service.jStorage="{}"}_storage_size=_storage_service.jStorage?String(_storage_service.jStorage).length:0;if(!_storage.__jstorage_meta){_storage.__jstorage_meta={}}if(!_storage.__jstorage_meta.CRC32){_storage.__jstorage_meta.CRC32={}}}function _save(){_dropOldEvents();try{_storage_service.jStorage=JSON.stringify(_storage);if(_storage_elm){_storage_elm.setAttribute("jStorage",_storage_service.jStorage);_storage_elm.save("jStorage")}_storage_size=_storage_service.jStorage?String(_storage_service.jStorage).length:0}catch(E7){}}function _checkKey(key){if(!key||(typeof key!="string"&&typeof key!="number")){throw new TypeError('Key name must be string or numeric')}if(key=="__jstorage_meta"){throw new TypeError('Reserved key name')}return true}function _handleTTL(){var curtime,i,TTL,CRC32,nextExpire=Infinity,changed=false,deleted=[];clearTimeout(_ttl_timeout);if(!_storage.__jstorage_meta||typeof _storage.__jstorage_meta.TTL!="object"){return}curtime=+new Date();TTL=_storage.__jstorage_meta.TTL;CRC32=_storage.__jstorage_meta.CRC32;for(i in TTL){if(TTL.hasOwnProperty(i)){if(TTL[i]<=curtime){delete TTL[i];delete CRC32[i];delete _storage[i];changed=true;deleted.push(i)}else if(TTL[i]<nextExpire){nextExpire=TTL[i]}}}if(nextExpire!=Infinity){_ttl_timeout=setTimeout(_handleTTL,nextExpire-curtime)}if(changed){_save();_publishChange();_fireObservers(deleted,"deleted")}}function _handlePubSub(){var i,len;if(!_storage.__jstorage_meta.PubSub){return}var pubelm,_pubsubCurrent=_pubsub_last;for(i=len=_storage.__jstorage_meta.PubSub.length-1;i>=0;i--){pubelm=_storage.__jstorage_meta.PubSub[i];if(pubelm[0]>_pubsub_last){_pubsubCurrent=pubelm[0];_fireSubscribers(pubelm[1],pubelm[2])}}_pubsub_last=_pubsubCurrent}function _fireSubscribers(channel,payload){if(_pubsub_observers[channel]){for(var i=0,len=_pubsub_observers[channel].length;i<len;i++){_pubsub_observers[channel][i](channel,JSON.parse(JSON.stringify(payload)))}}}function _dropOldEvents(){if(!_storage.__jstorage_meta.PubSub){return}var retire=+new Date()-2000;for(var i=0,len=_storage.__jstorage_meta.PubSub.length;i<len;i++){if(_storage.__jstorage_meta.PubSub[i][0]<=retire){_storage.__jstorage_meta.PubSub.splice(i,_storage.__jstorage_meta.PubSub.length-i);break}}if(!_storage.__jstorage_meta.PubSub.length){delete _storage.__jstorage_meta.PubSub}}function _publish(channel,payload){if(!_storage.__jstorage_meta){_storage.__jstorage_meta={}}if(!_storage.__jstorage_meta.PubSub){_storage.__jstorage_meta.PubSub=[]}_storage.__jstorage_meta.PubSub.unshift([+new Date,channel,payload]);_save();_publishChange()}function murmurhash2_32_gc(str,seed){var l=str.length,h=seed^l,i=0,k;while(l>=4){k=((str.charCodeAt(i)&0xff))|((str.charCodeAt(++i)&0xff)<<8)|((str.charCodeAt(++i)&0xff)<<16)|((str.charCodeAt(++i)&0xff)<<24);k=(((k&0xffff)*0x5bd1e995)+((((k>>>16)*0x5bd1e995)&0xffff)<<16));k^=k>>>24;k=(((k&0xffff)*0x5bd1e995)+((((k>>>16)*0x5bd1e995)&0xffff)<<16));h=(((h&0xffff)*0x5bd1e995)+((((h>>>16)*0x5bd1e995)&0xffff)<<16))^k;l-=4;++i}switch(l){case 3:h^=(str.charCodeAt(i+2)&0xff)<<16;case 2:h^=(str.charCodeAt(i+1)&0xff)<<8;case 1:h^=(str.charCodeAt(i)&0xff);h=(((h&0xffff)*0x5bd1e995)+((((h>>>16)*0x5bd1e995)&0xffff)<<16))}h^=h>>>13;h=(((h&0xffff)*0x5bd1e995)+((((h>>>16)*0x5bd1e995)&0xffff)<<16));h^=h>>>15;return h>>>0}$.jStorage={version:JSTORAGE_VERSION,set:function(key,value,options){_checkKey(key);options=options||{};if(typeof value=="undefined"){this.deleteKey(key);return value}if(_XMLService.isXML(value)){value={_is_xml:true,xml:_XMLService.encode(value)}}else if(typeof value=="function"){return undefined}else if(value&&typeof value=="object"){value=JSON.parse(JSON.stringify(value))}_storage[key]=value;_storage.__jstorage_meta.CRC32[key]="2."+murmurhash2_32_gc(JSON.stringify(value),0x9747b28c);this.setTTL(key,options.TTL||0);_fireObservers(key,"updated");return value},get:function(key,def){_checkKey(key);if(key in _storage){if(_storage[key]&&typeof _storage[key]=="object"&&_storage[key]._is_xml){return _XMLService.decode(_storage[key].xml)}else{return _storage[key]}}return typeof(def)=='undefined'?null:def},deleteKey:function(key){_checkKey(key);if(key in _storage){delete _storage[key];if(typeof _storage.__jstorage_meta.TTL=="object"&&key in _storage.__jstorage_meta.TTL){delete _storage.__jstorage_meta.TTL[key]}delete _storage.__jstorage_meta.CRC32[key];_save();_publishChange();_fireObservers(key,"deleted");return true}return false},setTTL:function(key,ttl){var curtime=+new Date();_checkKey(key);ttl=Number(ttl)||0;if(key in _storage){if(!_storage.__jstorage_meta.TTL){_storage.__jstorage_meta.TTL={}}if(ttl>0){_storage.__jstorage_meta.TTL[key]=curtime+ttl}else{delete _storage.__jstorage_meta.TTL[key]}_save();_handleTTL();_publishChange();return true}return false},getTTL:function(key){var curtime=+new Date(),ttl;_checkKey(key);if(key in _storage&&_storage.__jstorage_meta.TTL&&_storage.__jstorage_meta.TTL[key]){ttl=_storage.__jstorage_meta.TTL[key]-curtime;return ttl||0}return 0},flush:function(){_storage={__jstorage_meta:{CRC32:{}}};_save();_publishChange();_fireObservers(null,"flushed");return true},storageObj:function(){function F(){}F.prototype=_storage;return new F()},index:function(){var index=[],i;for(i in _storage){if(_storage.hasOwnProperty(i)&&i!="__jstorage_meta"){index.push(i)}}return index},storageSize:function(){return _storage_size},currentBackend:function(){return _backend},storageAvailable:function(){return!!_backend},listenKeyChange:function(key,callback){_checkKey(key);if(!_observers[key]){_observers[key]=[]}_observers[key].push(callback)},stopListening:function(key,callback){_checkKey(key);if(!_observers[key]){return}if(!callback){delete _observers[key];return}for(var i=_observers[key].length-1;i>=0;i--){if(_observers[key][i]==callback){_observers[key].splice(i,1)}}},subscribe:function(channel,callback){channel=(channel||"").toString();if(!channel){throw new TypeError('Channel not defined')}if(!_pubsub_observers[channel]){_pubsub_observers[channel]=[]}_pubsub_observers[channel].push(callback)},publish:function(channel,payload){channel=(channel||"").toString();if(!channel){throw new TypeError('Channel not defined')}_publish(channel,payload)},reInit:function(){_reloadData()}};_init()})();

=== modified file 'dashboard_app/templates/dashboard_app/image-report.html'
--- dashboard_app/templates/dashboard_app/image-report.html	2013-02-08 02:34:11 +0000
+++ dashboard_app/templates/dashboard_app/image-report.html	2013-06-10 15:33:53 +0000
@@ -4,79 +4,102 @@ 
 {{ block.super }}
 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-report.css"/>
 <script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/image-report.js"></script>
+<script src="{{ STATIC_URL }}dashboard_app/js/excanvas.min.js"></script>
+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.min.js"></script>
+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.dashes.min.js"></script>
+<script src="{{ STATIC_URL }}dashboard_app/js/jstorage.min.js"></script>
+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.tooltip.min.js"></script>
 {% endblock %}
 
 {% block content %}
 <h1>Image Report: {{ image.name }}</h1>
 
+
+<div id="outer-container">
+<div id="inner-container">
+</div>
+<div id="legend-container">
+</div>
+</div>
+
+
+
+<script language="javascript">
+  chart_data = $.parseJSON($('<div/>').html("{{chart_data}}").text());
+  test_names = $.parseJSON($('<div/>').html("{{test_names}}").text());
+  columns = $.parseJSON($('<div/>').html("{{columns}}").text());
+</script>
+
+<div id="filters">
+  <div id="filter_headline">Filters</div>
+  <div id="build_numbers_filter">
+    <div id="build_number_headline">
+      Start build number:
+    </div>
+    <span id="build_number_start_container">
+      <select id="build_number_start" onchange='update_table(columns, chart_data, test_names)'>
+      </select>
+    </span>
+
+    End build number:
+    <span id="build_number_end_container">
+      <select id="build_number_end" onchange='update_table(columns, chart_data, test_names)'>
+      </select>
+    </span>
+  </div>
+
+  <div id="tests_filter">
+    <div id="test_headline">
+      Tests:
+    </div>
+    <select id="test_select" onchange='update_table(columns, chart_data, test_names)' multiple>
+    </select>
+  </div>
+
+  <div id="target_goal_filter">
+    <div id="target_goal_headline">
+      Target Goal:
+    </div>
+    <input type="text" id="target_goal" onblur='update_table(columns, chart_data, test_names)' />
+  </div>
+
+  <div id="graph_type_filter">
+    <div id="graph_type_headline">
+      Graph type:
+    </div>
+    <input type="radio" name="graph_type" onclick='update_table(columns, chart_data, test_names)' checked value="percentage">
+    By percentage
+    </input>
+    <input type="radio" name="graph_type" onclick='update_table(columns, chart_data, test_names)' value="number">
+    By pass/fail test numbers
+    </input>
+  </div>
+</div>
+
+
 <table id="outer-table">
   <tr>
     <td>
       <table id="test-run-names" class="inner-table">
         <thead>
           <tr>
-            <th>
+            <th style='width: 170px;'>
               Build Number
             </th>
           </tr>
         </thead>
         <tbody>
-          <tr>
-            <td>
-              Date
-            </td>
-          </tr>
-          {% for test_run_name in test_run_names %}
-          <tr>
-            <td>
-              {{ test_run_name }}
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
+	</tbody>
       </table>
     </td>
     <td>
       <div id="scroller">
         <table id="results-table" class="inner-table">
           <thead>
-            <tr>
-              {% for col in cols %}
-              <th>
-                <a href="{{ col.link }}">{{ col.number }}</a>
-              </th>
-              {% endfor %}
-            </tr>
-          </thead>
-          <tbody>
-            <tr>
-              {% for col in cols %}
-              <td>
-                {{ col.date|date }}
-              </td>
-              {% endfor %}
-            </tr>
-            {% for row_data in table_data %}
-            <tr>
-              {% for result in row_data %}
-              <td class="{{ result.cls }}" data-uuid="{{ result.uuid }}">
-                {% if result.present %}
-                <a href="{{ result.link }}"> {{ result.passes }}/{{ result.total }} </a>
-                <span class="bug-links">
-                  {% for bug_id in result.bug_ids %}
-                  <a class="bug-link" href="https://bugs.launchpad.net/bugs/{{ bug_id }}" data-bug-id="{{ bug_id }}">[{{ bug_id }}]</a>
-                  {% endfor %}
-                  <a href="#" class="add-bug-link">[+]</a>
-                </span>
-                {% else %}
-                &mdash;
-                {% endif %}
-              </td>
-              {% endfor %}
-            </tr>
-            {% endfor %}
-          </tbody>
-        </table>
+	  </thead>
+	  <tbody>
+	  </tbody>
+	</table>
       </div>
     </td>
   </tr>

=== modified file 'dashboard_app/views/images.py'
--- dashboard_app/views/images.py	2013-02-08 02:35:13 +0000
+++ dashboard_app/views/images.py	2013-05-30 07:45:34 +0000
@@ -35,7 +35,7 @@ 
     TestRun,
 )
 from dashboard_app.views import index
-
+import json
 
 @BreadCrumb("Image Reports", parent=index)
 def image_report_list(request):
@@ -100,8 +100,8 @@ 
             if (match.tag, test_run.bundle.uploaded_on) not in build_number_to_cols:
                 build_number_to_cols[(match.tag, test_run.bundle.uploaded_on)] = {
                     'test_runs': {},
-                    'number': match.tag,
-                    'date': test_run.bundle.uploaded_on,
+                    'number': str(match.tag),
+                    'date': str(test_run.bundle.uploaded_on),
                     'link': test_run.bundle.get_absolute_url(),
                     }
             build_number_to_cols[(match.tag, test_run.bundle.uploaded_on)]['test_runs'][name] = test_run_data
@@ -114,7 +114,7 @@ 
 
     cols = [c for n, c in sorted(build_number_to_cols.items())]
 
-    table_data = []
+    table_data = {}
 
     for test_run_name in test_run_names:
         row_data = []
@@ -126,16 +126,16 @@ 
                     cls='missing',
                     )
             row_data.append(test_run_data)
-        table_data.append(row_data)
+        table_data[test_run_name] = row_data
 
     return render_to_response(
         "dashboard_app/image-report.html", {
             'bread_crumb_trail': BreadCrumbTrail.leading_to(
                 image_report_detail, name=image.name),
             'image': image,
-            'cols': cols,
-            'table_data': table_data,
-            'test_run_names': test_run_names,
+            'chart_data': json.dumps(table_data),
+            'test_names': json.dumps(test_run_names),
+            'columns': json.dumps(cols),
         }, RequestContext(request))