]>
Commit | Line | Data |
---|---|---|
31f18b77 FG |
1 | /* Flot plugin for plotting error bars. |
2 | ||
3 | Copyright (c) 2007-2013 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); |