/* Flot plugin for drawing filled areas around a central line. Released under the MIT license. Copyright (C) 2012 Ciengis, SA This plugin makes it esay to draw a central line surrounded by several areas. Each area corresponds to level around the line that can be configured using the fillArea property of each series. The configuration of the levels can be done as follows: { data: ..., fillArea : [ { // 1st level color: the area color; the default is the same color as the line opacity: 0-1; the default is: (n - i) / (n + 1) where n is the number of levels and i the level index representation: either "symmetric" or "asymmetric"; when "symmetric" is defined the level requires a single value that represents an offset around the line, when "asymmetric" is used the level will require 2 absolute values, the minimum and maximum. },{ // 2nd level color: ... opacity: ... representation: ... }, ..., {} ] } The size of the data points will be related with the number of levels and the values defined in representation. Example: var areaconf= [{color:"green", representation:"symmetric",opacity:0.5}, {color:"blue", representation:"asymmetric"}]; var mydata = []; for(var i = 0; i< 100; i++) { var x = i*2; var y = i+0.5; var stddev = 5.5;// use representation:"symmetric" var min = y- 10.0; // use representation:"asymmetric" var max = y+ 10.0;// use representation:"asymmetric" mydata[i] = [x,y,stddev, min, max]; } var dataset = [ {data:mydata, fillArea:areaconf, lines: { show: true, lineWidth: 2, fill: false, steps: true}, color:"brown", opacity:1} ]; $.plot($("#placeholder"), dataset); */ (function ($) { var options = { series: { fillArea: null } }; function init(plot) { /* This function checks if the point are valid and put them in order to * draw, in the points array. */ function processFillAreaData(plot, series, data, datapoints) { // copied from "jquery.flot.js" function updateAxis(axis, min, max) { if (min < axis.datamin && min != -fakeInfinity) axis.datamin = min; if (max > axis.datamax && max != fakeInfinity) axis.datamax = max; } // this plugin is applied to series with the fillArea property if(series.fillArea !== null && series.fillArea !== undefined) { // number of levels around the central line var nlevels = series.fillArea.length; // total number of values associated with one point var ps = nlevels * 2 +2 ; var format = formatNumbers(ps); datapoints.format = format; var steps = series.lines.steps; var size = ps * data.length; size = steps? size * 2 - 1: size; var points = new Array(size); datapoints.pointsize = ps; datapoints.points = points; var k = 0; // copy x and y from data to points for(var i = 0; i < data.length; i++) { var point = data[i]; // if the point is not defined/null then all values are null if (point === null || point === undefined) { for(var j = 0; j < ps;j++) { points[k+j] = null; } } else { points[k] = point[0]; // x points[k+1] = point[1]; // y } if(steps) { k += ps; } k += ps; } // determine the upper and lower values for each level and save // then in points var pos = 2; for(var l = 0; l < nlevels; l++) { k = 2 + l*2; if(series.fillArea[l].representation == "symmetric") { for(i=0; i 0 && i > points.length + ps) break; i += ps; // ps is negative if going backwards var x1 = points[i - ps], y1 = points[i - ps + ypos], x2 = points[i], y2 = points[i + ypos]; if (areaOpen) { if (ps > 0 && x1 != null && x2 == null) { // at turning point segmentEnd = i; ps = -ps; ypos = yLow; continue; } if (ps < 0 && i == segmentStart + ps) { // done with the reverse sweep ctx.fill(); areaOpen = false; ps = -ps; ypos = yUp; i = segmentStart = segmentEnd + ps; continue; } } if (x1 == null || x2 == null) continue; // clip x values // clip with xmin if (x1 <= x2 && x1 < axisx.min) { if (x2 < axisx.min) continue; y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.min; } else if (x2 <= x1 && x2 < axisx.min) { if (x1 < axisx.min) continue; y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.min; } // clip with xmax if (x1 >= x2 && x1 > axisx.max) { if (x2 > axisx.max) continue; y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x1 = axisx.max; } else if (x2 >= x1 && x2 > axisx.max) { if (x1 > axisx.max) continue; y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; x2 = axisx.max; } if (!areaOpen) { // open area ctx.beginPath(); ctx.moveTo(axisx.p2c(x1), axisy.p2c(y1)); areaOpen = true; } // now first check the case where both is outside if (y1 >= axisy.max && y2 >= axisy.max) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); continue; } else if (y1 <= axisy.min && y2 <= axisy.min) { ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); continue; } // else it's a bit more complicated, there might // be a flat maxed out rectangle first, then a // triangular cutout or reverse; to find these // keep track of the current x values var x1old = x1, x2old = x2; // clip the y values, without shortcutting, we // go through all cases in turn // clip with ymin if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.min; } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.min; } // clip with ymax if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y1 = axisy.max; } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; y2 = axisy.max; } // if the x value was changed we got a rectangle // to fill if (x1 != x1old) { ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); // it goes to (x1, y1), but we fill that below } // fill triangular section, this sometimes result // in redundant points if (x1, y1) hasn't changed // from previous line to, but we just ignore that ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); // fill the other rectangle if it's there if (x2 != x2old) { ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); } } } function levelOpacity(series, l) { var nlevels = series.fillArea.length; var opacity = series.fillArea[l].opacity; // if the opacity is not defined use the formula below if(opacity === null || opacity === undefined) opacity = (nlevels-l)/(nlevels+1); return opacity; } function levelColor(series, l) { var color = series.fillArea[l].color; // if the color is not defined use the color of the line if(color === null || color === undefined) color = series.color; return color; } /* Draws an area around the line. * It starts by drawing the areas farther from line so that the areas * closer to the line are always visible. */ function drawArea(plot, ctx, series) { if(series.fillArea !== null && series.fillArea !== undefined) { var plotOffset = plot.getPlotOffset(); ctx.save(); ctx.translate(plotOffset.left, plotOffset.top); var points = series.datapoints.points; var ps = series.datapoints.pointsize; //number of x and y's var xaxis = series.xaxis; var yaxis = series.yaxis; var nlevels = series.fillArea.length; var color, opacity; // areas on top of the line var yUp = ps-1; var yLow = yUp - 2; for(var l = nlevels-1; l > 0; l--) { color = levelColor(series, l); opacity = levelOpacity(series, l); plotLineArea(points, xaxis, yaxis, ctx, ps, yLow, yUp, color, opacity); yUp -= 2; yLow -= 2; } // areas on bottom of the line yUp = ps-2; yLow = yUp - 2; for(l = nlevels-1; l > 0; l--) { color = levelColor(series, l); opacity = levelOpacity(series, l); plotLineArea(points, xaxis, yaxis, ctx, ps, yLow, yUp, color, opacity); yUp -= 2; yLow -= 2; } // area around the line if(nlevels > 0) { color = levelColor(series, 0); opacity = levelOpacity(series, 0); plotLineArea(points, xaxis, yaxis, ctx, ps, 2, 3, color, opacity); } ctx.restore(); } } plot.hooks.processRawData.push(processFillAreaData); plot.hooks.drawSeries.push(drawArea); } $.plot.plugins.push({ init: init, options: options, name: 'fillarea', version: '1.0' }); })(jQuery);