Commit 22db28d3 by Torkel Ödegaard

Graph: New series style override option 'Fill below to', useful to visualize max…

Graph: New series style override option 'Fill below to', useful to visualize max & min as shadow for the mean, #940
parent 1330488e
...@@ -3,9 +3,12 @@ ...@@ -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({ ...@@ -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({ ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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([ ...@@ -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([ ...@@ -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([ ...@@ -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({ ...@@ -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({ ...@@ -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({ ...@@ -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'],
......
(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);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment