mirror of https://github.com/grafana/grafana.git
				
				
				
			Graph: New series style override option 'Fill below to', useful to visualize max & min as shadow for the mean, #940
This commit is contained in:
		
							parent
							
								
									1330488e13
								
							
						
					
					
						commit
						22db28d3e7
					
				|  | @ -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** | ||||
|  |  | |||
|  | @ -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'], | ||||
|  |  | |||
|  | @ -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; | ||||
|       } | ||||
|  |  | |||
|  | @ -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 = []; | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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]); | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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'], | ||||
|  |  | |||
|  | @ -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); | ||||
		Loading…
	
		Reference in New Issue