jquery.flot.errorbars.js 13 KB

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