Commit 3184942a by Alexander Zobnin Committed by Torkel Ödegaard

Fix NaN handling (#9469)

* graph: fix NaN formatting, #9012

* singlestat: prevent null value coloring, #9012, #8404

* timeseries: add tests for #9012 and move test file to TS
parent 473c47cd
define([ import {describe, beforeEach, it, expect} from 'test/lib/common';
'app/core/time_series' import TimeSeries from 'app/core/time_series2';
], function(TimeSeries) {
'use strict'; describe("TimeSeries", function() {
var points, series;
var yAxisFormats = ['short', 'ms'];
var testData;
beforeEach(function() {
testData = {
alias: 'test',
datapoints: [
[1,2],[null,3],[10,4],[8,5]
]
};
});
describe("TimeSeries", function() { describe('when getting flot pairs', function() {
var points, series; it('with connected style, should ignore nulls', function() {
var yAxisFormats = ['short', 'ms']; series = new TimeSeries(testData);
var testData; points = series.getFlotPairs('connected', yAxisFormats);
expect(points.length).to.be(3);
});
beforeEach(function() { it('with null as zero style, should replace nulls with zero', function() {
testData = { series = new TimeSeries(testData);
alias: 'test', points = series.getFlotPairs('null as zero', yAxisFormats);
datapoints: [ expect(points.length).to.be(4);
[1,2],[null,3],[10,4],[8,5] expect(points[1][1]).to.be(0);
]
};
}); });
describe('when getting flot pairs', function() { it('if last is null current should pick next to last', function() {
it('with connected style, should ignore nulls', function() { series = new TimeSeries({
series = new TimeSeries(testData); datapoints: [[10,1], [null, 2]]
points = series.getFlotPairs('connected', yAxisFormats);
expect(points.length).to.be(3);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.current).to.be(10);
});
it('with null as zero style, should replace nulls with zero', function() { it('max value should work for negative values', function() {
series = new TimeSeries(testData); series = new TimeSeries({
points = series.getFlotPairs('null as zero', yAxisFormats); datapoints: [[-10,1], [-4, 2]]
expect(points.length).to.be(4);
expect(points[1][1]).to.be(0);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.max).to.be(-4);
});
it('average value should ignore nulls', function() {
series = new TimeSeries(testData);
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.avg).to.be(6.333333333333333);
});
it('if last is null current should pick next to last', function() { it('the delta value should account for nulls', function() {
series = new TimeSeries({ series = new TimeSeries({
datapoints: [[10,1], [null, 2]] datapoints: [[1,2],[3,3],[null,4],[10,5],[15,6]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.current).to.be(10);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(14);
});
it('max value should work for negative values', function() { it('the delta value should account for nulls on first', function() {
series = new TimeSeries({ series = new TimeSeries({
datapoints: [[-10,1], [-4, 2]] datapoints: [[null,2],[1,3],[10,4],[15,5]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.max).to.be(-4);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(14);
});
it('average value should ignore nulls', function() { it('the delta value should account for nulls on last', function() {
series = new TimeSeries(testData); series = new TimeSeries({
series.getFlotPairs('null', yAxisFormats); datapoints: [[1,2],[5,3],[10,4],[null,5]]
expect(series.stats.avg).to.be(6.333333333333333);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(9);
});
it('the delta value should account for nulls', function() { it('the delta value should account for resets', function() {
series = new TimeSeries({ series = new TimeSeries({
datapoints: [[1,2],[3,3],[null,4],[10,5],[15,6]] datapoints: [[1,2],[5,3],[10,4],[0,5],[10,6]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(14);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(19);
});
it('the delta value should account for nulls on first', function() { it('the delta value should account for resets on last', function() {
series = new TimeSeries({ series = new TimeSeries({
datapoints: [[null,2],[1,3],[10,4],[15,5]] datapoints: [[1,2],[2,3],[10,4],[8,5]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(14);
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(17);
});
it('the delta value should account for nulls on last', function() { it('the range value should be max - min', function() {
series = new TimeSeries({ series = new TimeSeries(testData);
datapoints: [[1,2],[5,3],[10,4],[null,5]] series.getFlotPairs('null', yAxisFormats);
}); expect(series.stats.range).to.be(9);
series.getFlotPairs('null', yAxisFormats); });
expect(series.stats.delta).to.be(9);
it('first value should ingone nulls', function() {
series = new TimeSeries(testData);
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.first).to.be(1);
series = new TimeSeries({
datapoints: [[null,2],[1,3],[10,4],[8,5]]
}); });
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.first).to.be(1);
});
it('the delta value should account for resets', function() { it('with null as zero style, average value should treat nulls as 0', function() {
series = new TimeSeries({ series = new TimeSeries(testData);
datapoints: [[1,2],[5,3],[10,4],[0,5],[10,6]] series.getFlotPairs('null as zero', yAxisFormats);
}); expect(series.stats.avg).to.be(4.75);
series.getFlotPairs('null', yAxisFormats); });
expect(series.stats.delta).to.be(19);
it('average value should be null if all values is null', function() {
series = new TimeSeries({
datapoints: [[null,2],[null,3],[null,4],[null,5]]
}); });
series.getFlotPairs('null');
expect(series.stats.avg).to.be(null);
});
});
it('the delta value should account for resets on last', function() { describe('When checking if ms resolution is needed', function() {
series = new TimeSeries({ describe('msResolution with second resolution timestamps', function() {
datapoints: [[1,2],[2,3],[10,4],[8,5]] beforeEach(function() {
}); series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.delta).to.be(17);
}); });
it('the range value should be max - min', function() { it('should set hasMsResolution to false', function() {
series = new TimeSeries(testData); expect(series.hasMsResolution).to.be(false);
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.range).to.be(9);
}); });
});
it('first value should ingone nulls', function() { describe('msResolution with millisecond resolution timestamps', function() {
series = new TimeSeries(testData); beforeEach(function() {
series.getFlotPairs('null', yAxisFormats); series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]});
expect(series.stats.first).to.be(1);
series = new TimeSeries({
datapoints: [[null,2],[1,3],[10,4],[8,5]]
});
series.getFlotPairs('null', yAxisFormats);
expect(series.stats.first).to.be(1);
}); });
it('with null as zero style, average value should treat nulls as 0', function() { it('should show millisecond resolution tooltip', function() {
series = new TimeSeries(testData); expect(series.hasMsResolution).to.be(true);
series.getFlotPairs('null as zero', yAxisFormats);
expect(series.stats.avg).to.be(4.75);
}); });
}); });
describe('When checking if ms resolution is needed', function() { describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
describe('msResolution with second resolution timestamps', function() { beforeEach(function() {
beforeEach(function() { series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]});
series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]}); });
});
it('should set hasMsResolution to false', function() { it('should not show millisecond resolution tooltip', function() {
expect(series.hasMsResolution).to.be(false); expect(series.hasMsResolution).to.be(false);
});
}); });
});
});
describe('msResolution with millisecond resolution timestamps', function() { describe('can detect if series contains ms precision', function() {
beforeEach(function() { var fakedata;
series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]});
});
it('should show millisecond resolution tooltip', function() { beforeEach(function() {
expect(series.hasMsResolution).to.be(true); fakedata = testData;
}); });
});
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { it('missing datapoint with ms precision', function() {
beforeEach(function() { fakedata.datapoints[0] = [1337, 1234567890000];
series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]}); series = new TimeSeries(fakedata);
}); expect(series.isMsResolutionNeeded()).to.be(false);
});
it('should not show millisecond resolution tooltip', function() { it('contains datapoint with ms precision', function() {
expect(series.hasMsResolution).to.be(false); fakedata.datapoints[0] = [1337, 1236547890001];
}); series = new TimeSeries(fakedata);
}); expect(series.isMsResolutionNeeded()).to.be(true);
}); });
});
describe('can detect if series contains ms precision', function() { describe('series overrides', function() {
var fakedata; var series;
beforeEach(function() {
series = new TimeSeries(testData);
});
describe('fill & points', function() {
beforeEach(function() { beforeEach(function() {
fakedata = testData; series.alias = 'test';
}); series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
it('missing datapoint with ms precision', function() {
fakedata.datapoints[0] = [1337, 1234567890000];
series = new TimeSeries(fakedata);
expect(series.isMsResolutionNeeded()).to.be(false);
}); });
it('contains datapoint with ms precision', function() { it('should set fill zero, and enable points', function() {
fakedata.datapoints[0] = [1337, 1236547890001]; expect(series.lines.fill).to.be(0.001);
series = new TimeSeries(fakedata); expect(series.points.show).to.be(true);
expect(series.isMsResolutionNeeded()).to.be(true);
}); });
}); });
describe('series overrides', function() { describe('series option overrides, bars, true & lines false', function() {
var series;
beforeEach(function() { beforeEach(function() {
series = new TimeSeries(testData); series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
}); });
describe('fill & points', function() { it('should disable lines, and enable bars', function() {
beforeEach(function() { expect(series.lines.show).to.be(false);
series.alias = 'test'; expect(series.bars.show).to.be(true);
series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
});
it('should set fill zero, and enable points', function() {
expect(series.lines.fill).to.be(0.001);
expect(series.points.show).to.be(true);
});
}); });
});
describe('series option overrides, bars, true & lines false', function() { describe('series option overrides, linewidth, stack', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test'; series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]); series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
}); });
it('should disable lines, and enable bars', function() { it('should disable stack, and set lineWidth', function() {
expect(series.lines.show).to.be(false); expect(series.stack).to.be(false);
expect(series.bars.show).to.be(true); expect(series.lines.lineWidth).to.be(5);
});
}); });
});
describe('series option overrides, linewidth, stack', function() { describe('series option overrides, dashes and lineWidth', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test'; series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]); series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]);
}); });
it('should disable stack, and set lineWidth', function() { it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() {
expect(series.stack).to.be(false); expect(series.dashes.show).to.be(true);
expect(series.lines.lineWidth).to.be(5); expect(series.dashes.lineWidth).to.be(5);
}); expect(series.lines.lineWidth).to.be(0);
}); });
});
describe('series option overrides, dashes and lineWidth', function() { describe('series option overrides, fill below to', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test'; series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]); series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
}); });
it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() { it('should disable line fill and add fillBelowTo', function() {
expect(series.dashes.show).to.be(true); expect(series.fillBelowTo).to.be('min');
expect(series.dashes.lineWidth).to.be(5);
expect(series.lines.lineWidth).to.be(0);
});
}); });
});
describe('series option overrides, fill below to', function() { describe('series option overrides, pointradius, steppedLine', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test'; series.alias = 'test';
series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]); series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
}); });
it('should disable line fill and add fillBelowTo', function() { it('should set pointradius, and set steppedLine', function() {
expect(series.fillBelowTo).to.be('min'); expect(series.points.radius).to.be(5);
}); expect(series.lines.steps).to.be(true);
}); });
});
describe('series option overrides, pointradius, steppedLine', function() { describe('override match on regex', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test'; series.alias = 'test_01';
series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]); series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
}); });
it('should set pointradius, and set steppedLine', function() { it('should match second series', function() {
expect(series.points.radius).to.be(5); expect(series.lines.show).to.be(false);
expect(series.lines.steps).to.be(true);
});
}); });
});
describe('override match on regex', function() { describe('override series y-axis, and z-index', function() {
beforeEach(function() { beforeEach(function() {
series.alias = 'test_01'; series.alias = 'test';
series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]); series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
}); });
it('should match second series', function() { it('should set yaxis', function() {
expect(series.lines.show).to.be(false); expect(series.yaxis).to.be(2);
});
}); });
describe('override series y-axis, and z-index', function() { it('should set zindex', function() {
beforeEach(function() { expect(series.zindex).to.be(2);
series.alias = 'test'; });
series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]); });
});
it('should set yaxis', function() { });
expect(series.yaxis).to.be(2);
});
it('should set zindex', function() { describe('value formatter', function() {
expect(series.zindex).to.be(2); var series;
}); beforeEach(function() {
}); series = new TimeSeries(testData);
});
it('should format non-numeric values as empty string', function() {
expect(series.formatValue(null)).to.be("");
expect(series.formatValue(undefined)).to.be("");
expect(series.formatValue(NaN)).to.be("");
expect(series.formatValue(Infinity)).to.be("");
expect(series.formatValue(-Infinity)).to.be("");
}); });
}); });
}); });
...@@ -203,7 +203,7 @@ export default class TimeSeries { ...@@ -203,7 +203,7 @@ export default class TimeSeries {
if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; } if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; }
if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; } if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; }
if (result.length) { if (result.length && !this.allIsNull) {
this.stats.avg = (this.stats.total / nonNulls); this.stats.avg = (this.stats.total / nonNulls);
this.stats.current = result[result.length-1][1]; this.stats.current = result[result.length-1][1];
if (this.stats.current === null && result.length > 1) { if (this.stats.current === null && result.length > 1) {
...@@ -228,6 +228,9 @@ export default class TimeSeries { ...@@ -228,6 +228,9 @@ export default class TimeSeries {
} }
formatValue(value) { formatValue(value) {
if (!_.isFinite(value)) {
value = null; // Prevent NaN formatting
}
return this.valueFormater(value, this.decimals, this.scaledDecimals); return this.valueFormater(value, this.decimals, this.scaledDecimals);
} }
......
...@@ -606,7 +606,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -606,7 +606,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
var body = panel.gauge.show ? '' : getBigValueHtml(); var body = panel.gauge.show ? '' : getBigValueHtml();
if (panel.colorBackground && !isNaN(data.value)) { if (panel.colorBackground) {
var color = getColorForValue(data, data.value); var color = getColorForValue(data, data.value);
if (color) { if (color) {
$panelContainer.css('background-color', color); $panelContainer.css('background-color', color);
...@@ -690,6 +690,9 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -690,6 +690,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
function getColorForValue(data, value) { function getColorForValue(data, value) {
if (!_.isFinite(value)) {
return null;
}
for (var i = data.thresholds.length; i > 0; i--) { for (var i = data.thresholds.length; i > 0; i--) {
if (value >= data.thresholds[i-1]) { if (value >= data.thresholds[i-1]) {
return data.colorMap[i]; return data.colorMap[i];
......
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