Parcourir la source

Merge branch 'master' into valuepanel

Conflicts:
	src/app/components/settings.js
Torkel Ödegaard il y a 11 ans
Parent
commit
a1d764bd26

+ 5 - 0
CHANGELOG.md

@@ -3,8 +3,13 @@
 **UI Improvements*
 **UI Improvements*
 - [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
 - [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
 
 
+**Graph**
 - [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
 - [Issue #877](https://github.com/grafana/grafana/issues/877). Graph: Smart auto decimal precision when using scaled unit formats
 - [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
 - [Issue #850](https://github.com/grafana/grafana/issues/850). Graph: Shared tooltip that shows multiple series & crosshair line, thx @toni-moreno
+- [Issue #940](https://github.com/grafana/grafana/issues/940). Graph: New series style override option "Fill below to", useful to visualize max & min as a shadow for the mean
+
+**Misc**
+- [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory
 
 
 **Fixes**
 **Fixes**
 - [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other)
 - [Issue #925](https://github.com/grafana/grafana/issues/925). Graph: bar width calculation fix for some edge cases (bars would render on top of each other)

+ 0 - 1
src/app/app.js

@@ -57,7 +57,6 @@ function (angular, $, _, appLevelRequire, config) {
     register_fns.factory    = $provide.factory;
     register_fns.factory    = $provide.factory;
     register_fns.service    = $provide.service;
     register_fns.service    = $provide.service;
     register_fns.filter     = $filterProvider.register;
     register_fns.filter     = $filterProvider.register;
-
   });
   });
 
 
   var apps_deps = [
   var apps_deps = [

+ 2 - 0
src/app/components/require.config.js

@@ -40,6 +40,7 @@ require.config({
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
+    'jquery.flot.fillbelow':  '../vendor/jquery/jquery.flot.fillbelow',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
 
 
@@ -83,6 +84,7 @@ require.config({
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+    'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],
     'angular-dragdrop':     ['jquery', 'angular'],
     'angular-dragdrop':     ['jquery', 'angular'],
     'angular-loader':       ['angular'],
     'angular-loader':       ['angular'],

+ 5 - 2
src/app/components/settings.js

@@ -15,7 +15,10 @@ function (_, crypto) {
     var defaults = {
     var defaults = {
       datasources                   : {},
       datasources                   : {},
       window_title_prefix           : 'Grafana - ',
       window_title_prefix           : 'Grafana - ',
-      panels                        : ['graph', 'text', 'stats'],
+      panels                        : {
+        'graph': { path: 'panels/graph' },
+        'text': { path: 'panels/text' }
+      },
       plugins                       : {},
       plugins                       : {},
       default_route                 : '/dashboard/file/default.json',
       default_route                 : '/dashboard/file/default.json',
       playlist_timespan             : "1m",
       playlist_timespan             : "1m",
@@ -76,7 +79,7 @@ function (_, crypto) {
     });
     });
 
 
     if (settings.plugins.panels) {
     if (settings.plugins.panels) {
-      settings.panels = _.union(settings.panels, settings.plugins.panels);
+      _.extend(settings.panels, settings.plugins.panels);
     }
     }
 
 
     if (!settings.plugins.dependencies) {
     if (!settings.plugins.dependencies) {

+ 3 - 0
src/app/components/timeSeries.js

@@ -9,6 +9,7 @@ function (_, kbn) {
     this.datapoints = opts.datapoints;
     this.datapoints = opts.datapoints;
     this.info = opts.info;
     this.info = opts.info;
     this.label = opts.info.alias;
     this.label = opts.info.alias;
+    this.id = opts.info.alias;
     this.valueFormater = kbn.valueFormats.none;
     this.valueFormater = kbn.valueFormats.none;
     this.stats = {};
     this.stats = {};
   }
   }
@@ -50,6 +51,8 @@ function (_, kbn) {
       if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
       if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
       if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
       if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
       if (override.zindex !== void 0) { this.zindex = override.zindex; }
       if (override.zindex !== void 0) { this.zindex = override.zindex; }
+      if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; }
+
       if (override.yaxis !== void 0) {
       if (override.yaxis !== void 0) {
         this.info.yaxis = override.yaxis;
         this.info.yaxis = override.yaxis;
       }
       }

+ 5 - 14
src/app/controllers/dashboardCtrl.js

@@ -21,7 +21,7 @@ function (angular, $, config, _) {
       $timeout) {
       $timeout) {
 
 
     $scope.editor = { index: 0 };
     $scope.editor = { index: 0 };
-    $scope.panelNames = config.panels;
+    $scope.panelNames = _.map(config.panels, function(value, key) { return key; });
     var resizeEventTimeout;
     var resizeEventTimeout;
 
 
     this.init = function(dashboardData) {
     this.init = function(dashboardData) {
@@ -90,21 +90,12 @@ function (angular, $, config, _) {
       };
       };
     };
     };
 
 
-    $scope.edit_path = function(type) {
-      var p = $scope.panel_path(type);
-      if(p) {
-        return p+'/editor.html';
-      } else {
-        return false;
-      }
+    $scope.panelEditorPath = function(type) {
+      return 'app/' + config.panels[type].path + '/editor.html';
     };
     };
 
 
-    $scope.panel_path =function(type) {
-      if(type) {
-        return 'app/panels/'+type.replace(".","/");
-      } else {
-        return false;
-      }
+    $scope.pulldownEditorPath = function(type) {
+      return 'app/panels/'+type+'/editor.html';
     };
     };
 
 
     $scope.showJsonEditor = function(evt, options) {
     $scope.showJsonEditor = function(evt, options) {

+ 1 - 1
src/app/controllers/graphiteTarget.js

@@ -201,7 +201,7 @@ function (angular, _, config, gfunc, Parser) {
 
 
     $scope.targetTextChanged = function() {
     $scope.targetTextChanged = function() {
       parseTarget();
       parseTarget();
-      $scope.$parent.get_data();
+      $scope.get_data();
     };
     };
 
 
     $scope.targetChanged = function() {
     $scope.targetChanged = function() {

+ 1 - 0
src/app/directives/all.js

@@ -18,5 +18,6 @@ define([
   './templateParamSelector',
   './templateParamSelector',
   './graphiteSegment',
   './graphiteSegment',
   './grafanaVersionCheck',
   './grafanaVersionCheck',
+  './dropdown.typeahead',
   './influxdbFuncEditor'
   './influxdbFuncEditor'
 ], function () {});
 ], function () {});

+ 94 - 0
src/app/directives/dropdown.typeahead.js

@@ -0,0 +1,94 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+],
+function (angular, app, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('dropdownTypeahead', function($compile) {
+
+      var inputTemplate = '<input type="text"'+
+                            ' class="grafana-target-segment-input input-medium grafana-target-segment-input"' +
+                            ' spellcheck="false" style="display:none"></input>';
+
+      var buttonTemplate = '<a  class="grafana-target-segment grafana-target-function dropdown-toggle"' +
+                              ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
+                              ' data-placement="top"><i class="icon-plus"></i></a>';
+
+      return {
+        scope: {
+          "menuItems": "=dropdownTypeahead",
+          "dropdownTypeaheadOnSelect": "&dropdownTypeaheadOnSelect"
+        },
+        link: function($scope, elem) {
+          var $input = $(inputTemplate);
+          var $button = $(buttonTemplate);
+          $input.appendTo(elem);
+          $button.appendTo(elem);
+
+          var typeaheadValues = _.reduce($scope.menuItems, function(memo, value) {
+            _.each(value.submenu, function(item) {
+              memo.push(value.text + ' ' + item.text);
+            });
+            return memo;
+          }, []);
+
+          $input.attr('data-provide', 'typeahead');
+          $input.typeahead({
+            source: typeaheadValues,
+            minLength: 1,
+            items: 10,
+            updater: function (value) {
+              var result = {};
+              _.each($scope.menuItems, function(menuItem, optionIndex) {
+                _.each(menuItem.submenu, function(submenuItem, valueIndex) {
+                  if (value === (menuItem.text + ' ' + submenuItem.text)) {
+                    result.$item  = submenuItem;
+                    result.$optionIndex = optionIndex;
+                    result.$valueIndex = valueIndex;
+                  }
+                });
+              });
+
+              if (result.$item) {
+                $scope.$apply(function() {
+                  $scope.dropdownTypeaheadOnSelect(result);
+                });
+              }
+
+              $input.trigger('blur');
+              return '';
+            }
+          });
+
+          $button.click(function() {
+            $button.hide();
+            $input.show();
+            $input.focus();
+          });
+
+          $input.keyup(function() {
+            elem.toggleClass('open', $input.val() === '');
+          });
+
+          $input.blur(function() {
+            $input.hide();
+            $input.val('');
+            $button.show();
+            $button.focus();
+            // clicking the function dropdown menu wont
+            // work if you remove class at once
+            setTimeout(function() {
+              elem.removeClass('open');
+            }, 200);
+          });
+
+          $compile(elem.contents())($scope);
+        }
+      };
+    });
+});

+ 1 - 0
src/app/directives/grafanaGraph.js

@@ -177,6 +177,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             var series = data[i];
             var series = data[i];
             series.applySeriesOverrides(panel.seriesOverrides);
             series.applySeriesOverrides(panel.seriesOverrides);
             series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
             series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats);
+
             // if hidden remove points and disable stack
             // if hidden remove points and disable stack
             if (scope.hiddenSeries[series.info.alias]) {
             if (scope.hiddenSeries[series.info.alias]) {
               series.data = [];
               series.data = [];

+ 6 - 3
src/app/directives/grafanaPanel.js

@@ -1,9 +1,10 @@
 define([
 define([
   'angular',
   'angular',
   'jquery',
   'jquery',
+  'config',
   './panelMenu',
   './panelMenu',
 ],
 ],
-function (angular, $) {
+function (angular, $, config) {
   'use strict';
   'use strict';
 
 
   angular
   angular
@@ -68,10 +69,12 @@ function (angular, $) {
 
 
           elem.addClass('ng-cloak');
           elem.addClass('ng-cloak');
 
 
+          var panelPath = config.panels[panelType].path;
+
           $scope.require([
           $scope.require([
             'jquery',
             'jquery',
-            'text!panels/'+panelType+'/module.html',
-            'panels/' + panelType + "/module",
+            'text!'+panelPath+'/module.html',
+            panelPath + "/module",
           ], function ($, moduleTemplate) {
           ], function ($, moduleTemplate) {
             var $module = $(moduleTemplate);
             var $module = $(moduleTemplate);
             $module.prepend(panelHeader);
             $module.prepend(panelHeader);

+ 3 - 2
src/app/panels/graph/module.js

@@ -16,6 +16,7 @@ define([
   'jquery.flot.time',
   'jquery.flot.time',
   'jquery.flot.stack',
   'jquery.flot.stack',
   'jquery.flot.stackpercent',
   'jquery.flot.stackpercent',
+  'jquery.flot.fillbelow',
   'jquery.flot.crosshair'
   'jquery.flot.crosshair'
 ],
 ],
 function (angular, app, $, _, kbn, moment, TimeSeries) {
 function (angular, app, $, _, kbn, moment, TimeSeries) {
@@ -340,8 +341,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries) {
       $scope.render();
       $scope.render();
     };
     };
 
 
-    $scope.addSeriesOverride = function() {
-      $scope.panel.seriesOverrides.push({});
+    $scope.addSeriesOverride = function(override) {
+      $scope.panel.seriesOverrides.push(override || {});
     };
     };
 
 
     $scope.removeSeriesOverride = function(override) {
     $scope.removeSeriesOverride = function(override) {

+ 9 - 0
src/app/panels/graph/seriesOverridesCtrl.js

@@ -34,6 +34,14 @@ define([
       var option = $scope.overrideMenu[optionIndex];
       var option = $scope.overrideMenu[optionIndex];
       var value = option.values[valueIndex];
       var value = option.values[valueIndex];
       $scope.override[option.propertyName] = value;
       $scope.override[option.propertyName] = value;
+
+      // automatically disable lines for this series and the fill bellow to series
+      // can be removed by the user if they still want lines
+      if (option.propertyName === 'fillBelowTo') {
+        $scope.override['lines'] = false;
+        $scope.addSeriesOverride({ alias: value, lines: false });
+      }
+
       $scope.updateCurrentOverrides();
       $scope.updateCurrentOverrides();
       $scope.render();
       $scope.render();
     };
     };
@@ -67,6 +75,7 @@ define([
     $scope.addOverrideOption('Lines', 'lines', [true, false]);
     $scope.addOverrideOption('Lines', 'lines', [true, false]);
     $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
     $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]);
     $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
     $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]);
+    $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
     $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
     $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
     $scope.addOverrideOption('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);

+ 3 - 4
src/app/panels/graph/styleEditor.html

@@ -88,11 +88,10 @@
 							<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
 							<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
 							{{option.name}}: {{option.value}}
 							{{option.name}}: {{option.value}}
 						</li>
 						</li>
-						<li class="dropdown">
-							<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'" data-placement="top">
-								<i class="icon-plus"></i>
-							</a>
+
+						<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($optionIndex, $valueIndex)">
 						</li>
 						</li>
+
 					</ul>
 					</ul>
 					<div class="clearfix"></div>
 					<div class="clearfix"></div>
 				</div>
 				</div>

+ 1 - 1
src/app/partials/dasheditor.html

@@ -84,7 +84,7 @@
 		</div>
 		</div>
 
 
 		<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
 		<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
-			<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
+			<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
 			<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
 			<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
 		</div>
 		</div>
 
 

+ 1 - 1
src/app/partials/paneleditor.html

@@ -17,7 +17,7 @@
 	</div>
 	</div>
 
 
 	<div ng-show="editorTabs[editor.index] == 'Panel'">
 	<div ng-show="editorTabs[editor.index] == 'Panel'">
-		<div ng-include src="edit_path(panel.type)"></div>
+		<div ng-include src="panelEditorPath(panel.type)"></div>
 	</div>
 	</div>
 
 
 	<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
 	<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">

+ 10 - 0
src/plugins/custom.panel.example/editor.html

@@ -0,0 +1,10 @@
+<div>
+  <div class="row-fluid">
+    <div class="span4">
+      <label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
+    </div>
+    <div class="span2" ng-show="panel.mode == 'text'">
+      <label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
+    </div>
+  </div>
+</div>

+ 3 - 0
src/plugins/custom.panel.example/module.html

@@ -0,0 +1,3 @@
+<div ng-controller='CustomPanelCtrl'>
+	<h2>Custom panel</h2>
+</div>

+ 31 - 0
src/plugins/custom.panel.example/module.js

@@ -0,0 +1,31 @@
+define([
+  'angular',
+  'app',
+  'lodash',
+  'require',
+],
+function (angular, app, _) {
+  'use strict';
+
+  var module = angular.module('grafana.panels.custom', []);
+  app.useModule(module);
+
+  module.controller('CustomPanelCtrl', function($scope, panelSrv) {
+
+    $scope.panelMeta = {
+      description : "Example plugin panel",
+    };
+
+    // set and populate defaults
+    var _d = {
+    };
+
+    _.defaults($scope.panel, _d);
+
+    $scope.init = function() {
+      panelSrv.init($scope);
+    };
+
+    $scope.init();
+  });
+});

+ 11 - 0
src/test/specs/timeSeries-specs.js

@@ -70,6 +70,17 @@ define([
         });
         });
       });
       });
 
 
+      describe('series option overrides, fill below to', function() {
+        beforeEach(function() {
+          series.info.alias = 'test';
+          series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
+        });
+
+        it('should disable line fill and add fillBelowTo', function() {
+          expect(series.fillBelowTo).to.be('min');
+        });
+      });
+
       describe('series option overrides, pointradius, steppedLine', function() {
       describe('series option overrides, pointradius, steppedLine', function() {
         beforeEach(function() {
         beforeEach(function() {
           series.info.alias = 'test';
           series.info.alias = 'test';

+ 2 - 1
src/test/test-main.js

@@ -42,6 +42,7 @@ require.config({
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
+    'jquery.flot.fillbelow':  '../vendor/jquery/jquery.flot.fillbelow',
 
 
     modernizr:                '../vendor/modernizr-2.6.1',
     modernizr:                '../vendor/modernizr-2.6.1',
   },
   },
@@ -68,7 +69,6 @@ require.config({
       exports: 'Crypto'
       exports: 'Crypto'
     },
     },
 
 
-    'jquery-ui':            ['jquery'],
     'jquery.flot':          ['jquery'],
     'jquery.flot':          ['jquery'],
     'jquery.flot.pie':      ['jquery', 'jquery.flot'],
     'jquery.flot.pie':      ['jquery', 'jquery.flot'],
     'jquery.flot.events':   ['jquery', 'jquery.flot'],
     'jquery.flot.events':   ['jquery', 'jquery.flot'],
@@ -77,6 +77,7 @@ require.config({
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+    'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
 
 
     'angular-route':        ['angular'],
     'angular-route':        ['angular'],
     'angular-cookies':      ['angular'],
     'angular-cookies':      ['angular'],

+ 288 - 0
src/vendor/jquery/jquery.flot.fillbelow.js

@@ -0,0 +1,288 @@
+(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 preceeds 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 preceeding 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);