Browse Source

Graph: New series style override option 'Fill below to', useful to visualize max & min as shadow for the mean, #940

Torkel Ödegaard 11 năm trước cách đây
mục cha
commit
22db28d3e7

+ 4 - 1
CHANGELOG.md

@@ -3,9 +3,12 @@
 **UI Improvements*
 - [Issue #770](https://github.com/grafana/grafana/issues/770). UI: Panel dropdown menu replaced with a new panel menu
 
-**Misc**
+**Graph**
 - [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 #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**

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

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

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

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

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

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

+ 1 - 0
src/app/panels/graph/module.js

@@ -16,6 +16,7 @@ define([
   'jquery.flot.time',
   'jquery.flot.stack',
   'jquery.flot.stackpercent',
+  'jquery.flot.fillbelow',
   'jquery.flot.crosshair'
 ],
 function (angular, app, $, _, kbn, moment, TimeSeries) {

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

@@ -67,6 +67,7 @@ define([
     $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 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('Points', 'points', [true, false]);
     $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]);

+ 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() {
         beforeEach(function() {
           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.time':       '../vendor/jquery/jquery.flot.time',
     'jquery.flot.crosshair':  '../vendor/jquery/jquery.flot.crosshair',
+    'jquery.flot.fillbelow':  '../vendor/jquery/jquery.flot.fillbelow',
 
     modernizr:                '../vendor/modernizr-2.6.1',
   },
@@ -68,7 +69,6 @@ require.config({
       exports: 'Crypto'
     },
 
-    'jquery-ui':            ['jquery'],
     'jquery.flot':          ['jquery'],
     'jquery.flot.pie':      ['jquery', 'jquery.flot'],
     'jquery.flot.events':   ['jquery', 'jquery.flot'],
@@ -77,6 +77,7 @@ require.config({
     'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
     'jquery.flot.time':     ['jquery', 'jquery.flot'],
     'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+    'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
 
     'angular-route':        ['angular'],
     'angular-cookies':      ['angular'],

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

@@ -0,0 +1,289 @@
+(function($) {
+    "use strict";
+
+    var options = {
+        series: {
+            fillBelowTo: null
+        }
+    };
+
+    function init(plot) {
+        function findBelowSeries( series, allseries ) {
+
+            var i;
+
+            debugger;
+            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);