From 6f65c38880d102230818e3a2b986538ad78e36d3 Mon Sep 17 00:00:00 2001 From: Danilo Freitas Date: Mon, 5 Mar 2012 22:57:55 -0300 Subject: [PATCH] Implement error bars using standard deviation on timeline plots jqplot.ohlcRendererWithErrorBars.min.js is the standard OHLC Renderer plugin from jqPlot 1.0.9 (revision d96a669) with the error-bar addition given here: https://bitbucket.org/cleonello/jqplot/issues/35/add-error-bar-capability#comment-141580 --- codespeed/settings.py | 1 + .../jqplot.ohlcRendererWithErrorBars.min.js | 1 + codespeed/static/js/timeline.js | 61 +++++++++++++++++-- codespeed/templates/codespeed/timeline.html | 8 +++ codespeed/views.py | 7 +++ 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 codespeed/static/js/jqplot/jqplot.ohlcRendererWithErrorBars.min.js 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;ie.max||null==e.max)&&(e.max=r[i][1])}else for(var i=0;ie.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;ii;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:'
date:%s
open:%s
hi:%s
low:%s
close:%s
'})}}(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 @@ + {% if use_error_bars %} + + {% endif %} {% if use_median_bands %}