mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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*
|
**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
|
||||||
|
|
||||||
**Misc**
|
**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
|
- [Issue #938](https://github.com/grafana/grafana/issues/938). Panel: Plugin panels now reside outside of app/panels directory
|
||||||
|
|
||||||
**Fixes**
|
**Fixes**
|
||||||
|
@ -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'],
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 = [];
|
||||||
|
@ -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) {
|
||||||
|
@ -67,6 +67,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]);
|
||||||
|
@ -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';
|
||||||
|
@ -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'],
|
||||||
|
289
src/vendor/jquery/jquery.flot.fillbelow.js
vendored
Normal file
289
src/vendor/jquery/jquery.flot.fillbelow.js
vendored
Normal file
@ -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
Block a user