jquery.flot.fillbelow.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. (function($) {
  2. "use strict";
  3. var options = {
  4. series: {
  5. fillBelowTo: null
  6. }
  7. };
  8. function init(plot) {
  9. function findBelowSeries( series, allseries ) {
  10. var i;
  11. for ( i = 0; i < allseries.length; ++i ) {
  12. if ( allseries[ i ].id === series.fillBelowTo ) {
  13. return allseries[ i ];
  14. }
  15. }
  16. return null;
  17. }
  18. /* top and bottom doesn't actually matter for this, we're just using it to help make this easier to think about */
  19. /* this is a vector cross product operation */
  20. 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) {
  21. var top_delta_x, top_delta_y, bottom_delta_x, bottom_delta_y,
  22. s, t;
  23. top_delta_x = top_right_x - top_left_x;
  24. top_delta_y = top_right_y - top_left_y;
  25. bottom_delta_x = bottom_right_x - bottom_left_x;
  26. bottom_delta_y = bottom_right_y - bottom_left_y;
  27. s = (
  28. (-top_delta_y * (top_left_x - bottom_left_x)) + (top_delta_x * (top_left_y - bottom_left_y))
  29. ) / (
  30. -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y
  31. );
  32. t = (
  33. (bottom_delta_x * (top_left_y - bottom_left_y)) - (bottom_delta_y * (top_left_x - bottom_left_x))
  34. ) / (
  35. -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y
  36. );
  37. // Collision detected
  38. if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
  39. return [
  40. top_left_x + (t * top_delta_x), // X
  41. top_left_y + (t * top_delta_y) // Y
  42. ];
  43. }
  44. // No collision
  45. return null;
  46. }
  47. function plotDifferenceArea(plot, ctx, series) {
  48. if ( series.fillBelowTo === null ) {
  49. return;
  50. }
  51. var otherseries,
  52. ps,
  53. points,
  54. otherps,
  55. otherpoints,
  56. plotOffset,
  57. fillStyle;
  58. function openPolygon(x, y) {
  59. ctx.beginPath();
  60. ctx.moveTo(
  61. series.xaxis.p2c(x) + plotOffset.left,
  62. series.yaxis.p2c(y) + plotOffset.top
  63. );
  64. }
  65. function closePolygon() {
  66. ctx.closePath();
  67. ctx.fill();
  68. }
  69. function validateInput() {
  70. if (points.length/ps !== otherpoints.length/otherps) {
  71. console.error("Refusing to graph inconsistent number of points");
  72. return false;
  73. }
  74. var i;
  75. for (i = 0; i < (points.length / ps); i++) {
  76. if (
  77. points[i * ps] !== null &&
  78. otherpoints[i * otherps] !== null &&
  79. points[i * ps] !== otherpoints[i * otherps]
  80. ) {
  81. console.error("Refusing to graph points without matching value");
  82. return false;
  83. }
  84. }
  85. return true;
  86. }
  87. function findNextStart(start_i, end_i) {
  88. console.assert(end_i > start_i, "expects the end index to be greater than the start index");
  89. var start = (
  90. start_i === 0 ||
  91. points[start_i - 1] === null ||
  92. otherpoints[start_i - 1] === null
  93. ),
  94. equal = false,
  95. i,
  96. intersect;
  97. for (i = start_i; i < end_i; i++) {
  98. // Take note of null points
  99. if (
  100. points[(i * ps) + 1] === null ||
  101. otherpoints[(i * ps) + 1] === null
  102. ) {
  103. equal = false;
  104. start = true;
  105. }
  106. // Take note of equal points
  107. else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) {
  108. equal = true;
  109. start = false;
  110. }
  111. else if (points[(i * ps) + 1] > otherpoints[(i * otherps) + 1]) {
  112. // If we begin above the desired point
  113. if (start) {
  114. openPolygon(points[i * ps], points[(i * ps) + 1]);
  115. }
  116. // If an equal point precedes this, start the polygon at that equal point
  117. else if (equal) {
  118. openPolygon(points[(i - 1) * ps], points[((i - 1) * ps) + 1]);
  119. }
  120. // Otherwise, find the intersection point, and start it there
  121. else {
  122. intersect = intersectionPoint(i);
  123. openPolygon(intersect[0], intersect[1]);
  124. }
  125. topTraversal(i, end_i);
  126. return;
  127. }
  128. // If we go below equal, equal at any preceding point is irrelevant
  129. else {
  130. start = false;
  131. equal = false;
  132. }
  133. }
  134. }
  135. function intersectionPoint(right_i) {
  136. console.assert(right_i > 0, "expects the second point in the series line segment");
  137. var i, intersect;
  138. for (i = 1; i < (otherpoints.length/otherps); i++) {
  139. intersect = segmentIntersection(
  140. points[(right_i - 1) * ps], points[((right_i - 1) * ps) + 1],
  141. points[right_i * ps], points[(right_i * ps) + 1],
  142. otherpoints[(i - 1) * otherps], otherpoints[((i - 1) * otherps) + 1],
  143. otherpoints[i * otherps], otherpoints[(i * otherps) + 1]
  144. );
  145. if (intersect !== null) {
  146. return intersect;
  147. }
  148. }
  149. console.error("intersectionPoint() should only be called when an intersection happens");
  150. }
  151. function bottomTraversal(start_i, end_i) {
  152. 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)");
  153. var i;
  154. for (i = start_i; i >= end_i; i--) {
  155. ctx.lineTo(
  156. otherseries.xaxis.p2c(otherpoints[i * otherps]) + plotOffset.left,
  157. otherseries.yaxis.p2c(otherpoints[(i * otherps) + 1]) + plotOffset.top
  158. );
  159. }
  160. closePolygon();
  161. }
  162. function topTraversal(start_i, end_i) {
  163. 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)");
  164. var i,
  165. intersect;
  166. for (i = start_i; i < end_i; i++) {
  167. if (points[(i * ps) + 1] === null && i > start_i) {
  168. bottomTraversal(i - 1, start_i);
  169. findNextStart(i, end_i);
  170. return;
  171. }
  172. else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) {
  173. bottomTraversal(i, start_i);
  174. findNextStart(i, end_i);
  175. return;
  176. }
  177. else if (points[(i * ps) + 1] < otherpoints[(i * otherps) + 1]) {
  178. intersect = intersectionPoint(i);
  179. ctx.lineTo(
  180. series.xaxis.p2c(intersect[0]) + plotOffset.left,
  181. series.yaxis.p2c(intersect[1]) + plotOffset.top
  182. );
  183. bottomTraversal(i, start_i);
  184. findNextStart(i, end_i);
  185. return;
  186. }
  187. else {
  188. ctx.lineTo(
  189. series.xaxis.p2c(points[i * ps]) + plotOffset.left,
  190. series.yaxis.p2c(points[(i * ps) + 1]) + plotOffset.top
  191. );
  192. }
  193. }
  194. bottomTraversal(end_i, start_i);
  195. }
  196. // Begin processing
  197. otherseries = findBelowSeries( series, plot.getData() );
  198. if ( !otherseries ) {
  199. return;
  200. }
  201. ps = series.datapoints.pointsize;
  202. points = series.datapoints.points;
  203. otherps = otherseries.datapoints.pointsize;
  204. otherpoints = otherseries.datapoints.points;
  205. plotOffset = plot.getPlotOffset();
  206. if (!validateInput()) {
  207. return;
  208. }
  209. // Flot's getFillStyle() should probably be exposed somewhere
  210. fillStyle = $.color.parse(series.color);
  211. fillStyle.a = 0.4;
  212. fillStyle.normalize();
  213. ctx.fillStyle = fillStyle.toString();
  214. // Begin recursive bi-directional traversal
  215. findNextStart(0, points.length/ps);
  216. }
  217. plot.hooks.drawSeries.push(plotDifferenceArea);
  218. }
  219. $.plot.plugins.push({
  220. init: init,
  221. options: options,
  222. name: "fillbelow",
  223. version: "0.1.0"
  224. });
  225. })(jQuery);