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*
|
||||
- [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'],
|
||||
|
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