1 /* Flot plugin for plotting error bars. 2 3 Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 Licensed under the MIT license. 5 6 Error bars are used to show standard deviation and other statistical 7 properties in a plot. 8 9 * Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com 10 11 This plugin allows you to plot error-bars over points. Set "errorbars" inside 12 the points series to the axis name over which there will be error values in 13 your data array (*even* if you do not intend to plot them later, by setting 14 "show: null" on xerr/yerr). 15 16 The plugin supports these options: 17 18 series: { 19 points: { 20 errorbars: "x" or "y" or "xy", 21 xerr: { 22 show: null/false or true, 23 asymmetric: null/false or true, 24 upperCap: null or "-" or function, 25 lowerCap: null or "-" or function, 26 color: null or color, 27 radius: null or number 28 }, 29 yerr: { same options as xerr } 30 } 31 } 32 33 Each data point array is expected to be of the type: 34 35 "x" [ x, y, xerr ] 36 "y" [ x, y, yerr ] 37 "xy" [ x, y, xerr, yerr ] 38 39 Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and 40 equivalently for yerr. Eg., a datapoint for the "xy" case with symmetric 41 error-bars on X and asymmetric on Y would be: 42 43 [ x, y, xerr, yerr_lower, yerr_upper ] 44 45 By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will 46 draw a small cap perpendicular to the error bar. They can also be set to a 47 user-defined drawing function, with (ctx, x, y, radius) as parameters, as eg. 48 49 function drawSemiCircle( ctx, x, y, radius ) { 50 ctx.beginPath(); 51 ctx.arc( x, y, radius, 0, Math.PI, false ); 52 ctx.moveTo( x - radius, y ); 53 ctx.lineTo( x + radius, y ); 54 ctx.stroke(); 55 } 56 57 Color and radius both default to the same ones of the points series if not 58 set. The independent radius parameter on xerr/yerr is useful for the case when 59 we may want to add error-bars to a line, without showing the interconnecting 60 points (with radius: 0), and still showing end caps on the error-bars. 61 shadowSize and lineWidth are derived as well from the points series. 62 63 */ 64 65 (function ($) { 66 var options = { 67 series: { 68 points: { 69 errorbars: null, //should be 'x', 'y' or 'xy' 70 xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, 71 yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} 72 } 73 } 74 }; 75 76 function processRawData(plot, series, data, datapoints){ 77 if (!series.points.errorbars) 78 return; 79 80 // x,y values 81 var format = [ 82 { x: true, number: true, required: true }, 83 { y: true, number: true, required: true } 84 ]; 85 86 var errors = series.points.errorbars; 87 // error bars - first X then Y 88 if (errors == 'x' || errors == 'xy') { 89 // lower / upper error 90 if (series.points.xerr.asymmetric) { 91 format.push({ x: true, number: true, required: true }); 92 format.push({ x: true, number: true, required: true }); 93 } else 94 format.push({ x: true, number: true, required: true }); 95 } 96 if (errors == 'y' || errors == 'xy') { 97 // lower / upper error 98 if (series.points.yerr.asymmetric) { 99 format.push({ y: true, number: true, required: true }); 100 format.push({ y: true, number: true, required: true }); 101 } else 102 format.push({ y: true, number: true, required: true }); 103 } 104 datapoints.format = format; 105 } 106 107 function parseErrors(series, i){ 108 109 var points = series.datapoints.points; 110 111 // read errors from points array 112 var exl = null, 113 exu = null, 114 eyl = null, 115 eyu = null; 116 var xerr = series.points.xerr, 117 yerr = series.points.yerr; 118 119 var eb = series.points.errorbars; 120 // error bars - first X 121 if (eb == 'x' || eb == 'xy') { 122 if (xerr.asymmetric) { 123 exl = points[i + 2]; 124 exu = points[i + 3]; 125 if (eb == 'xy') 126 if (yerr.asymmetric){ 127 eyl = points[i + 4]; 128 eyu = points[i + 5]; 129 } else eyl = points[i + 4]; 130 } else { 131 exl = points[i + 2]; 132 if (eb == 'xy') 133 if (yerr.asymmetric) { 134 eyl = points[i + 3]; 135 eyu = points[i + 4]; 136 } else eyl = points[i + 3]; 137 } 138 // only Y 139 } else if (eb == 'y') 140 if (yerr.asymmetric) { 141 eyl = points[i + 2]; 142 eyu = points[i + 3]; 143 } else eyl = points[i + 2]; 144 145 // symmetric errors? 146 if (exu == null) exu = exl; 147 if (eyu == null) eyu = eyl; 148 149 var errRanges = [exl, exu, eyl, eyu]; 150 // nullify if not showing 151 if (!xerr.show){ 152 errRanges[0] = null; 153 errRanges[1] = null; 154 } 155 if (!yerr.show){ 156 errRanges[2] = null; 157 errRanges[3] = null; 158 } 159 return errRanges; 160 } 161 162 function drawSeriesErrors(plot, ctx, s){ 163 164 var points = s.datapoints.points, 165 ps = s.datapoints.pointsize, 166 ax = [s.xaxis, s.yaxis], 167 radius = s.points.radius, 168 err = [s.points.xerr, s.points.yerr]; 169 170 //sanity check, in case some inverted axis hack is applied to flot 171 var invertX = false; 172 if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { 173 invertX = true; 174 var tmp = err[0].lowerCap; 175 err[0].lowerCap = err[0].upperCap; 176 err[0].upperCap = tmp; 177 } 178 179 var invertY = false; 180 if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { 181 invertY = true; 182 var tmp = err[1].lowerCap; 183 err[1].lowerCap = err[1].upperCap; 184 err[1].upperCap = tmp; 185 } 186 187 for (var i = 0; i < s.datapoints.points.length; i += ps) { 188 189 //parse 190 var errRanges = parseErrors(s, i); 191 192 //cycle xerr & yerr 193 for (var e = 0; e < err.length; e++){ 194 195 var minmax = [ax[e].min, ax[e].max]; 196 197 //draw this error? 198 if (errRanges[e * err.length]){ 199 200 //data coordinates 201 var x = points[i], 202 y = points[i + 1]; 203 204 //errorbar ranges 205 var upper = [x, y][e] + errRanges[e * err.length + 1], 206 lower = [x, y][e] - errRanges[e * err.length]; 207 208 //points outside of the canvas 209 if (err[e].err == 'x') 210 if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) 211 continue; 212 if (err[e].err == 'y') 213 if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) 214 continue; 215 216 // prevent errorbars getting out of the canvas 217 var drawUpper = true, 218 drawLower = true; 219 220 if (upper > minmax[1]) { 221 drawUpper = false; 222 upper = minmax[1]; 223 } 224 if (lower < minmax[0]) { 225 drawLower = false; 226 lower = minmax[0]; 227 } 228 229 //sanity check, in case some inverted axis hack is applied to flot 230 if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { 231 //swap coordinates 232 var tmp = lower; 233 lower = upper; 234 upper = tmp; 235 tmp = drawLower; 236 drawLower = drawUpper; 237 drawUpper = tmp; 238 tmp = minmax[0]; 239 minmax[0] = minmax[1]; 240 minmax[1] = tmp; 241 } 242 243 // convert to pixels 244 x = ax[0].p2c(x), 245 y = ax[1].p2c(y), 246 upper = ax[e].p2c(upper); 247 lower = ax[e].p2c(lower); 248 minmax[0] = ax[e].p2c(minmax[0]); 249 minmax[1] = ax[e].p2c(minmax[1]); 250 251 //same style as points by default 252 var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, 253 sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; 254 255 //shadow as for points 256 if (lw > 0 && sw > 0) { 257 var w = sw / 2; 258 ctx.lineWidth = w; 259 ctx.strokeStyle = "rgba(0,0,0,0.1)"; 260 drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); 261 262 ctx.strokeStyle = "rgba(0,0,0,0.2)"; 263 drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); 264 } 265 266 ctx.strokeStyle = err[e].color? err[e].color: s.color; 267 ctx.lineWidth = lw; 268 //draw it 269 drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); 270 } 271 } 272 } 273 } 274 275 function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ 276 277 //shadow offset 278 y += offset; 279 upper += offset; 280 lower += offset; 281 282 // error bar - avoid plotting over circles 283 if (err.err == 'x'){ 284 if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); 285 else drawUpper = false; 286 if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); 287 else drawLower = false; 288 } 289 else { 290 if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); 291 else drawUpper = false; 292 if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); 293 else drawLower = false; 294 } 295 296 //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps 297 //this is a way to get errorbars on lines without visible connecting dots 298 radius = err.radius != null? err.radius: radius; 299 300 // upper cap 301 if (drawUpper) { 302 if (err.upperCap == '-'){ 303 if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); 304 else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); 305 } else if ($.isFunction(err.upperCap)){ 306 if (err.err=='x') err.upperCap(ctx, upper, y, radius); 307 else err.upperCap(ctx, x, upper, radius); 308 } 309 } 310 // lower cap 311 if (drawLower) { 312 if (err.lowerCap == '-'){ 313 if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); 314 else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); 315 } else if ($.isFunction(err.lowerCap)){ 316 if (err.err=='x') err.lowerCap(ctx, lower, y, radius); 317 else err.lowerCap(ctx, x, lower, radius); 318 } 319 } 320 } 321 322 function drawPath(ctx, pts){ 323 ctx.beginPath(); 324 ctx.moveTo(pts[0][0], pts[0][1]); 325 for (var p=1; p < pts.length; p++) 326 ctx.lineTo(pts[p][0], pts[p][1]); 327 ctx.stroke(); 328 } 329 330 function draw(plot, ctx){ 331 var plotOffset = plot.getPlotOffset(); 332 333 ctx.save(); 334 ctx.translate(plotOffset.left, plotOffset.top); 335 $.each(plot.getData(), function (i, s) { 336 if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) 337 drawSeriesErrors(plot, ctx, s); 338 }); 339 ctx.restore(); 340 } 341 342 function init(plot) { 343 plot.hooks.processRawData.push(processRawData); 344 plot.hooks.draw.push(draw); 345 } 346 347 $.plot.plugins.push({ 348 init: init, 349 options: options, 350 name: 'errorbars', 351 version: '1.0' 352 }); 353 })(jQuery); 354