diff --git a/codespeed/settings.py b/codespeed/settings.py index 7a577809..707b3f1c 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -63,6 +63,7 @@ # ('myexe', '21df2423ra'), # ('myexe', 'L'),] +USE_ERROR_BARS = True # True to enable error bars on Timeline view USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view diff --git a/codespeed/static/js/jqplot/jqplot.ohlcRendererWithErrorBars.min.js b/codespeed/static/js/jqplot/jqplot.ohlcRendererWithErrorBars.min.js new file mode 100644 index 00000000..f4eb2f9e --- /dev/null +++ b/codespeed/static/js/jqplot/jqplot.ohlcRendererWithErrorBars.min.js @@ -0,0 +1 @@ +!function(t){t.jqplot.OHLCRenderer=function(){t.jqplot.LineRenderer.call(this),this.candleStick=!1,this.tickLength="auto",this.bodyWidth="auto",this.openColor=null,this.closeColor=null,this.wickColor=null,this.fillUpBody=!1,this.fillDownBody=!0,this.upBodyColor=null,this.downBodyColor=null,this.hlc=!1,this.lineWidth=1.5,this._tickLength,this._bodyWidth},t.jqplot.OHLCRenderer.prototype=new t.jqplot.LineRenderer,t.jqplot.OHLCRenderer.prototype.constructor=t.jqplot.OHLCRenderer,t.jqplot.OHLCRenderer.prototype.init=function(o){o=o||{},this.lineWidth=o.lineWidth||1.5,t.jqplot.LineRenderer.prototype.init.call(this,o),this._type="ohlc";var e=this._yaxis._dataBounds,r=this._plotData;if(r[0].length<5){this.renderer.hlc=!0;for(var i=0;i<r.length;i++)(r[i][2]<e.min||null==e.min)&&(e.min=r[i][2]),(r[i][1]>e.max||null==e.max)&&(e.max=r[i][1])}else for(var i=0;i<r.length;i++)(r[i][3]<e.min||null==e.min)&&(e.min=r[i][3]),(r[i][2]>e.max||null==e.max)&&(e.max=r[i][2])},t.jqplot.OHLCRenderer.prototype.draw=function(o,e,r){var i,l,d,n,h,s,a,c,p,y=this.data,w=this._xaxis.min,k=this._xaxis.max,C=0,u=y.length,g=this._xaxis.series_u2p,_=this._yaxis.series_u2p,f=this.renderer,R=void 0!=r?r:{};void 0!=R.shadow?R.shadow:this.shadow,void 0!=R.fill?R.fill:this.fill,void 0!=R.fillAndStroke?R.fillAndStroke:this.fillAndStroke;if(f.bodyWidth=void 0!=R.bodyWidth?R.bodyWidth:f.bodyWidth,f.tickLength=void 0!=R.tickLength?R.tickLength:f.tickLength,o.save(),this.show){for(var L,x,v,m,B,i=0;i<y.length;i++)y[i][0]<w?C=i:y[i][0]<k&&(u=i+1);var b=this.gridData[u-1][0]-this.gridData[C][0],W=u-C;try{var j=Math.abs(this._xaxis.series_u2p(parseInt(this._xaxis._intervalStats[0].sortedIntervals[0].interval,10))-this._xaxis.series_u2p(0))}catch(q){var j=b/W}f.candleStick?"number"==typeof f.bodyWidth?f._bodyWidth=f.bodyWidth:f._bodyWidth=Math.min(20,j/1.65):"number"==typeof f.tickLength?f._tickLength=f.tickLength:f._tickLength=Math.min(10,j/3.5);for(var i=C;u>i;i++)L=g(y[i][0]),f.hlc?(x=null,v=_(y[i][1]),m=_(y[i][2]),B=_(y[i][3])):(x=_(y[i][1]),v=_(y[i][2]),m=_(y[i][3]),B=_(y[i][4])),p={},f.candleStick&&!f.hlc?(s=f._bodyWidth,a=L-s/2,x>B?(f.wickColor?p.color=f.wickColor:f.downBodyColor&&(p.color=f.upBodyColor),d=t.extend(!0,{},R,p),f.shapeRenderer.draw(o,[[L,v],[L,B]],d),f.shapeRenderer.draw(o,[[L,x],[L,m]],d),p={},n=B,h=x-B,f.fillUpBody?p.fillRect=!0:(p.strokeRect=!0,s-=this.lineWidth,a=L-s/2),f.upBodyColor&&(p.color=f.upBodyColor,p.fillStyle=f.upBodyColor),c=[a,n,s,h]):B>x?(f.wickColor?p.color=f.wickColor:f.downBodyColor&&(p.color=f.downBodyColor),d=t.extend(!0,{},R,p),f.shapeRenderer.draw(o,[[L,v],[L,x]],d),f.shapeRenderer.draw(o,[[L,B],[L,m]],d),p={},n=x,h=B-x,f.fillDownBody?p.fillRect=!0:(p.strokeRect=!0,s-=this.lineWidth,a=L-s/2),f.downBodyColor&&(p.color=f.downBodyColor,p.fillStyle=f.downBodyColor),c=[a,n,s,h]):(f.wickColor&&(p.color=f.wickColor),d=t.extend(!0,{},R,p),f.shapeRenderer.draw(o,[[L,v],[L,m]],d),p={},p.fillRect=!1,p.strokeRect=!1,a=[L-s/2,x],n=[L+s/2,B],s=null,h=null,c=[a,n]),d=t.extend(!0,{},R,p),f.shapeRenderer.draw(o,c,d)):f.errorBar?(l=R.color,f.openColor&&(R.color=f.openColor),R.color=l,f.wickColor&&(R.color=f.wickColor),f.shapeRenderer.draw(o,[[L,v],[L,m]],R),R.color=l,f.shapeRenderer.draw(o,[[L+f._tickLength/2,v],[L-f._tickLength/2,v]],R),f.shapeRenderer.draw(o,[[L+f._tickLength/2,m],[L-f._tickLength/2,m]],R),R.fillRect=!0,f.shapeRenderer.draw(o,[Math.round(L-f._tickLength/4),Math.round(B-f._tickLength/4),f._tickLength/2,f._tickLength/2],R),R.color=l,R.fillRect=!1):(l=R.color,f.openColor&&(R.color=f.openColor),f.hlc||f.shapeRenderer.draw(o,[[L-f._tickLength,x],[L,x]],R),R.color=l,f.wickColor&&(R.color=f.wickColor),f.shapeRenderer.draw(o,[[L,v],[L,m]],R),R.color=l,f.closeColor&&(R.color=f.closeColor),f.shapeRenderer.draw(o,[[L,B],[L+f._tickLength,B]],R),R.color=l)}o.restore()},t.jqplot.OHLCRenderer.prototype.drawShadow=function(t,o,e){},t.jqplot.OHLCRenderer.checkOptions=function(t,o,e){e.highlighter||(e.highlighter={showMarker:!1,tooltipAxes:"y",yvalues:4,formatString:'<table class="jqplot-highlighter"><tr><td>date:</td><td>%s</td></tr><tr><td>open:</td><td>%s</td></tr><tr><td>hi:</td><td>%s</td></tr><tr><td>low:</td><td>%s</td></tr><tr><td>close:</td><td>%s</td></tr></table>'})}}(jQuery); \ No newline at end of file diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index 568261bb..3ff3638c 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -34,6 +34,10 @@ function shouldPlotEquidistant() { return $("#equidistant").is(':checked'); } +function shouldPlotErrorBars() { + return $("#show_error_bars").is(':checked'); +} + function shouldPlotQuartiles() { return $("#show_quartile_bands").is(':checked'); } @@ -50,6 +54,7 @@ function getConfiguration() { env: $("input[name='environments']:checked").val(), revs: $("#revisions option:selected").val(), equid: $("#equidistant").is(':checked') ? "on" : "off", + error: $("#show_error_bars").is(':checked') ? "on" : "off", quarts: $("#show_quartile_bands").is(':checked') ? "on" : "off", extr: $("#show_extrema_bands").is(':checked') ? "on" : "off" }; @@ -98,13 +103,34 @@ function getHighlighterConfig(median) { function renderPlot(data) { var plotdata = [], series = [], + firstdates = [], + lastdates = [], lastvalues = [];//hopefully the smallest values for determining significant digits. seriesindex = []; + var errorSeries = 0; var hiddenSeries = 0; + var mean = data['data_type'] === 'U'; var median = data['data_type'] === 'M'; for (var branch in data.branches) { // NOTE: Currently, only the "default" branch is shown in the timeline for (var exe_id in data.branches[branch]) { + if (mean) { + $("span.options.mean").css("display", "inline"); + if (shouldPlotErrorBars()) { + marker = false; + var error = new Array(); + for (res in data["branches"][branch][exe_id]) { + var date = data["branches"][branch][exe_id][res][0]; + var value = data["branches"][branch][exe_id][res][1]; + var std_dev = data["branches"][branch][exe_id][res][2]; + error.push([date, value - std_dev, value + std_dev, data["branches"][branch][exe_id][res][3]]); + } + plotdata.push(error); + series.push({renderer:$.jqplot.OHLCRenderer, rendererOptions:{errorBar:true}, showLabel: false, showMarker: true, + "label": $("label[for*='executable" + getColor(exe_id) + "']").html() + " error", color: "#C0C0C0"}); + errorSeries++; + } + } // FIXME if (branch !== "default") { label += " - " + branch; } var label = $("label[for*='executable" + exe_id + "']").html(); var seriesConfig = { @@ -153,8 +179,15 @@ function renderPlot(data) { } series.push(seriesConfig); seriesindex.push(exe_id); - plotdata.push(data.branches[branch][exe_id]); - lastvalues.push(data.branches[branch][exe_id][0][1]); + var exeData = data.branches[branch][exe_id]; + plotdata.push(exeData); + var startDate = new Date(exeData[exeData.length - 1][0]) + var endDate = new Date(exeData[0][0]); + startDate.setDate(startDate.getDate() - 1); + endDate.setDate(endDate.getDate() + 1); + firstdates.push(startDate); + lastdates.push(endDate); + lastvalues.push(exeData[0][1]); } //determine significant digits var digits = 2; @@ -202,7 +235,8 @@ function renderPlot(data) { labelRenderer: $.jqplot.CanvasAxisLabelRenderer, tickOptions: {formatString:'%b %d'}, pad: 1.01, - autoscale: true, + min: Math.min.apply(Math, firstdates), + max: Math.max.apply(Math, lastdates), rendererOptions: {sortMergedLabels:true} /* only relevant when $.jqplot.CategoryAxisRenderer is used */ } @@ -211,7 +245,7 @@ function renderPlot(data) { highlighter: getHighlighterConfig(median), cursor: {show:true, zoom:true, showTooltip:false, clickReset:true} }; - if (series.length > 4 + hiddenSeries) { + if (series.length > 4 + errorSeries + hiddenSeries) { // Move legend outside plot area to unclutter var labels = []; for (var l in series) { @@ -231,7 +265,9 @@ function renderPlot(data) { function renderMiniplot(plotid, data) { var plotdata = [], - series = []; + series = [], + firstdates = [], + lastdates = []; for (var branch in data.branches) { for (var id in data.branches[branch]) { @@ -239,6 +275,13 @@ function renderMiniplot(plotid, data) { "label": $("label[for*='executable" + id + "']").html(), "color": getColor(id) }); + var exeData = data.branches[branch][id]; + var startDate = new Date(exeData[exeData.length - 1][0]) + var endDate = new Date(exeData[0][0]); + startDate.setDate(startDate.getDate() - 1); + endDate.setDate(endDate.getDate() + 1); + firstdates.push(startDate); + lastdates.push(endDate); plotdata.push(data.branches[branch][id]); } } @@ -268,7 +311,10 @@ function renderMiniplot(plotid, data) { renderer:$.jqplot.DateAxisRenderer, pad: 1.01, autoscale:true, - showTicks: false + showTicks: false, + min: Math.min.apply(Math, firstdates), + max: Math.max.apply(Math, lastdates), + rendererOptions: {sortMergedLabels:true} } }, highlighter: {show:false}, @@ -280,6 +326,7 @@ function renderMiniplot(plotid, data) { function render(data) { $("#revisions").attr("disabled", false); $("#equidistant").attr("disabled", false); + $("span.options.mean").css("display", "none"); $("span.options.median").css("display", "none"); $("#plotgrid").html(""); if(data.error !== "None") { @@ -342,6 +389,7 @@ function initializeSite(event) { $("input[name='benchmark']" ).change(updateUrl); $("input[name='environments']").change(updateUrl); $("#equidistant" ).change(updateUrl); + $("#show_error_bars" ).change(updateUrl); $("#show_quartile_bands" ).change(updateUrl); $("#show_extrema_bands" ).change(updateUrl); } @@ -397,6 +445,7 @@ function setValuesOfInputFields(event) { $("#baselinecolor").css("background-color", baselineColor); $("#equidistant").prop('checked', valueOrDefault(event.parameters.equid, defaults.equidistant) === "on"); + $("#show_error_bars").prop('checked', valueOrDefault(event.parameters.error, defaults.error) === "on"); $("#show_quartile_bands").prop('checked', valueOrDefault(event.parameters.quarts, defaults.quartiles) === "on"); $("#show_extrema_bands").prop('checked', valueOrDefault(event.parameters.extr, defaults.extrema) === "on"); } diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index 88eefce1..b5ed86b4 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -85,6 +85,12 @@ <input id="equidistant" name="equidistant" type="checkbox" /> <label for="equidistant">Equidistant</label> </span> + {% if use_error_bars %} + <span class="options mean" title="Shows error bars in the plots" style="display: none"> + <input id="show_error_bars" type="checkbox" name="show_error_bars" checked="checked"/> + <label for="show_error_bars">Show error bars</label> + </span> + {% endif %} {% if use_median_bands %} <span class="options median" title="Shows quartile bands in the plots" style="display: none"> <input id="show_quartile_bands" type="checkbox" name="show_quartile_bands" checked="checked"/> @@ -114,6 +120,7 @@ <script type="text/javascript" src="{{ STATIC_URL }}js/jqplot/jqplot.categoryAxisRenderer.min.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}js/jqplot/jqplot.canvasTextRenderer.min.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}js/jqplot/jqplot.canvasAxisLabelRenderer.min.js"></script> +<script type="text/javascript" src="{{ STATIC_URL }}js/jqplot/jqplot.ohlcRendererWithErrorBars.min.js"></script> <script type="text/javascript"> var CHANGES_URL = "{% url "changes" %}"; </script> @@ -128,6 +135,7 @@ benchmark: "{{ defaultbenchmark }}", environment: {{ defaultenvironment.id }}, equidistant: "{{ defaultequid }}", + error: "{{ defaulterr }}", quartiles: "{{ defaultquarts }}", extrema: "{{ defaultextr }}" }); diff --git a/codespeed/views.py b/codespeed/views.py index cd5773df..9e7fe414 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -466,6 +466,10 @@ def timeline(request): defaultequid = data['equid'] else: defaultequid = "off" + if 'error' in data: + defaulterr = data['error'] + else: + defaulterr = "on" if 'quarts' in data: defaultquarts = data['quarts'] else: @@ -479,6 +483,7 @@ def timeline(request): executables = {} for proj in Project.objects.filter(track=True): executables[proj] = Executable.objects.filter(project=proj) + use_error_bars = hasattr(settings, 'USE_ERROR_BARS') and settings.USE_ERROR_BARS use_median_bands = hasattr(settings, 'USE_MEDIAN_BANDS') and settings.USE_MEDIAN_BANDS return render_to_response('codespeed/timeline.html', { 'checkedexecutables': checkedexecutables, @@ -494,8 +499,10 @@ def timeline(request): 'branch_list': branch_list, 'defaultbranch': defaultbranch, 'defaultequid': defaultequid, + 'defaulterr': defaulterr, 'defaultquarts': defaultquarts, 'defaultextr': defaultextr, + 'use_error_bars': use_error_bars, 'use_median_bands': use_median_bands, }, context_instance=RequestContext(request))