(function($) { "use strict"; var options = { series: { fillBelowTo: null } }; function init(plot) { function findBelowSeries( series, allseries ) { var i; for ( i = 0; i < allseries.length; ++i ) { if ( allseries[ i ].id === series.fillBelowTo ) { return allseries[ i ]; } } return null; } /* top and bottom doesn't actually matter for this, we're just using it to help make this easier to think about */ /* this is a vector cross product operation */ function segmentIntersection(top_left_x, top_left_y, top_right_x, top_right_y, bottom_left_x, bottom_left_y, bottom_right_x, bottom_right_y) { var top_delta_x, top_delta_y, bottom_delta_x, bottom_delta_y, s, t; top_delta_x = top_right_x - top_left_x; top_delta_y = top_right_y - top_left_y; bottom_delta_x = bottom_right_x - bottom_left_x; bottom_delta_y = bottom_right_y - bottom_left_y; s = ( (-top_delta_y * (top_left_x - bottom_left_x)) + (top_delta_x * (top_left_y - bottom_left_y)) ) / ( -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y ); t = ( (bottom_delta_x * (top_left_y - bottom_left_y)) - (bottom_delta_y * (top_left_x - bottom_left_x)) ) / ( -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y ); // Collision detected if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { return [ top_left_x + (t * top_delta_x), // X top_left_y + (t * top_delta_y) // Y ]; } // No collision return null; } function plotDifferenceArea(plot, ctx, series) { if ( series.fillBelowTo === null ) { return; } var otherseries, ps, points, otherps, otherpoints, plotOffset, fillStyle; function openPolygon(x, y) { ctx.beginPath(); ctx.moveTo( series.xaxis.p2c(x) + plotOffset.left, series.yaxis.p2c(y) + plotOffset.top ); } function closePolygon() { ctx.closePath(); ctx.fill(); } function validateInput() { if (points.length/ps !== otherpoints.length/otherps) { console.error("Refusing to graph inconsistent number of points"); return false; } var i; for (i = 0; i < (points.length / ps); i++) { if ( points[i * ps] !== null && otherpoints[i * otherps] !== null && points[i * ps] !== otherpoints[i * otherps] ) { console.error("Refusing to graph points without matching value"); return false; } } return true; } function findNextStart(start_i, end_i) { console.assert(end_i > start_i, "expects the end index to be greater than the start index"); var start = ( start_i === 0 || points[start_i - 1] === null || otherpoints[start_i - 1] === null ), equal = false, i, intersect; for (i = start_i; i < end_i; i++) { // Take note of null points if ( points[(i * ps) + 1] === null || otherpoints[(i * ps) + 1] === null ) { equal = false; start = true; } // Take note of equal points else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { equal = true; start = false; } else if (points[(i * ps) + 1] > otherpoints[(i * otherps) + 1]) { // If we begin above the desired point if (start) { openPolygon(points[i * ps], points[(i * ps) + 1]); } // If an equal point precedes this, start the polygon at that equal point else if (equal) { openPolygon(points[(i - 1) * ps], points[((i - 1) * ps) + 1]); } // Otherwise, find the intersection point, and start it there else { intersect = intersectionPoint(i); openPolygon(intersect[0], intersect[1]); } topTraversal(i, end_i); return; } // If we go below equal, equal at any preceding point is irrelevant else { start = false; equal = false; } } } function intersectionPoint(right_i) { console.assert(right_i > 0, "expects the second point in the series line segment"); var i, intersect; for (i = 1; i < (otherpoints.length/otherps); i++) { intersect = segmentIntersection( points[(right_i - 1) * ps], points[((right_i - 1) * ps) + 1], points[right_i * ps], points[(right_i * ps) + 1], otherpoints[(i - 1) * otherps], otherpoints[((i - 1) * otherps) + 1], otherpoints[i * otherps], otherpoints[(i * otherps) + 1] ); if (intersect !== null) { return intersect; } } console.error("intersectionPoint() should only be called when an intersection happens"); } function bottomTraversal(start_i, end_i) { console.assert(start_i >= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)"); var i; for (i = start_i; i >= end_i; i--) { ctx.lineTo( otherseries.xaxis.p2c(otherpoints[i * otherps]) + plotOffset.left, otherseries.yaxis.p2c(otherpoints[(i * otherps) + 1]) + plotOffset.top ); } closePolygon(); } function topTraversal(start_i, end_i) { console.assert(start_i <= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)"); var i, intersect; for (i = start_i; i < end_i; i++) { if (points[(i * ps) + 1] === null && i > start_i) { bottomTraversal(i - 1, start_i); findNextStart(i, end_i); return; } else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) { bottomTraversal(i, start_i); findNextStart(i, end_i); return; } else if (points[(i * ps) + 1] < otherpoints[(i * otherps) + 1]) { intersect = intersectionPoint(i); ctx.lineTo( series.xaxis.p2c(intersect[0]) + plotOffset.left, series.yaxis.p2c(intersect[1]) + plotOffset.top ); bottomTraversal(i, start_i); findNextStart(i, end_i); return; } else { ctx.lineTo( series.xaxis.p2c(points[i * ps]) + plotOffset.left, series.yaxis.p2c(points[(i * ps) + 1]) + plotOffset.top ); } } bottomTraversal(end_i, start_i); } // Begin processing otherseries = findBelowSeries( series, plot.getData() ); if ( !otherseries ) { return; } ps = series.datapoints.pointsize; points = series.datapoints.points; otherps = otherseries.datapoints.pointsize; otherpoints = otherseries.datapoints.points; plotOffset = plot.getPlotOffset(); if (!validateInput()) { return; } // Flot's getFillStyle() should probably be exposed somewhere fillStyle = $.color.parse(series.color); fillStyle.a = 0.4; fillStyle.normalize(); ctx.fillStyle = fillStyle.toString(); // Begin recursive bi-directional traversal findNextStart(0, points.length/ps); } plot.hooks.drawSeries.push(plotDifferenceArea); } $.plot.plugins.push({ init: init, options: options, name: "fillbelow", version: "0.1.0" }); })(jQuery);