OLD | NEW |
(Empty) | |
| 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 t
o 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.line
Width, |
| 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, dr
awLower, 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, dr
awLower, 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, drawLo
wer, 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 + radiu
s,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 - rad
ius,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 dot
s |
| 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 + r
adius]] ); |
| 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 + r
adius]] ); |
| 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); |
OLD | NEW |