diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index add09f6b3ba..54db6be5838 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -44,7 +44,7 @@ function convertEsSeriesToHeatmap(series: TimeSeries, saveZeroCounts = false) { buckets: valueBuckets }; - // Don't push buckets vith 0 count until saveZeroCounts flag is set + // Don't push buckets with 0 count until saveZeroCounts flag is set if (count !== 0 || (count === 0 && saveZeroCounts)) { xBuckets.push(xBucket); } @@ -404,11 +404,55 @@ function getMinLog(series) { return _.min(values); } -// Logarithm for custom base +/** + * Logarithm for custom base + * @param value + * @param base logarithm base + */ function logp(value, base) { return Math.log(value) / Math.log(base); } +/** + * Calculate size of Y bucket from given buckets bounds. + * @param bounds Array of Y buckets bounds + * @param logBase Logarithm base + */ +function calculateBucketSize(bounds: number[], logBase = 1): number { + let bucketSize = Infinity; + + if (bounds.length === 0) { + return 0; + } else if (bounds.length === 1) { + return bounds[0]; + } else { + bounds = _.sortBy(bounds); + for (let i = 1; i < bounds.length; i++) { + let distance = getDistance(bounds[i], bounds[i - 1], logBase); + bucketSize = distance < bucketSize ? distance : bucketSize; + } + } + + return bucketSize; +} + +/** + * Calculate distance between two numbers in given scale (linear or logarithmic). + * @param a + * @param b + * @param logBase + */ +function getDistance(a: number, b: number, logBase = 1): number { + if (logBase === 1) { + // Linear distance + return Math.abs(b - a); + } else { + // logarithmic distance + let ratio = Math.max(a, b) / Math.min(a, b); + return logp(ratio, logBase); + } +} + /** * Compare two heatmap data objects * @param objA @@ -427,7 +471,7 @@ function isHeatmapDataEqual(objA: any, objB: any): boolean { _.forEach(xBucket.buckets, (yBucket: YBucket, y) => { if (objB[x].buckets && objB[x].buckets[y]) { if (objB[x].buckets[y].values) { - is_eql = _.isEqual(yBucket.values.sort(), objB[x].buckets[y].values.sort()); + is_eql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values)); if (!is_eql) { return false; } @@ -465,5 +509,6 @@ export { mergeZeroBuckets, getMinLog, getValueBucketBound, - isHeatmapDataEqual + isHeatmapDataEqual, + calculateBucketSize }; diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts index 2f37fd04dd2..b1019b94c56 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common'; import TimeSeries from 'app/core/time_series2'; -import { convertToHeatMap, elasticHistogramToHeatmap, isHeatmapDataEqual } from '../heatmap_data_converter'; +import { convertToHeatMap, elasticHistogramToHeatmap, calculateBucketSize, isHeatmapDataEqual } from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { let ctx: any = {}; @@ -64,6 +64,55 @@ describe('isHeatmapDataEqual', () => { }); }); +describe('calculateBucketSize', () => { + let ctx: any = {}; + + describe('when logBase is 1 (linear scale)', () => { + + beforeEach(() => { + ctx.logBase = 1; + ctx.bounds_set = [ + { bounds: [], size: 0 }, + { bounds: [0], size: 0 }, + { bounds: [4], size: 4 }, + { bounds: [0, 1, 2, 3, 4], size: 1 }, + { bounds: [0, 1, 3, 5, 7], size: 1 }, + { bounds: [0, 3, 7, 9, 15], size: 2 }, + { bounds: [0, 7, 3, 15, 9], size: 2 }, + { bounds: [0, 5, 10, 15, 50], size: 5 } + ]; + }); + + it('should properly calculate bucket size', () => { + _.each(ctx.bounds_set, (b) => { + let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + expect(bucketSize).to.be(b.size); + }); + }); + }); + + describe('when logBase is 2', () => { + + beforeEach(() => { + ctx.logBase = 2; + ctx.bounds_set = [ + { bounds: [], size: 0 }, + { bounds: [0], size: 0 }, + { bounds: [4], size: 4 }, + { bounds: [1, 2, 4, 8], size: 1 }, + { bounds: [1, Math.SQRT2, 2, 8, 16], size: 0.5 } + ]; + }); + + it('should properly calculate bucket size', () => { + _.each(ctx.bounds_set, (b) => { + let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + expect(isEqual(bucketSize, b.size)).to.be(true); + }); + }); + }); +}); + describe('HeatmapDataConverter', () => { let ctx: any = {}; @@ -184,9 +233,21 @@ describe('ES Histogram converter', () => { }; let heatmap = elasticHistogramToHeatmap(ctx.series); - console.log(heatmap); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true); }); }); }); +/** + * Compare two numbers with given precision. Suitable for compare float numbers after conversions with precision loss. + * @param a + * @param b + * @param precision + */ +function isEqual(a: number, b: number, precision = 0.000001): boolean { + if (a === b) { + return true; + } else { + return Math.abs(1 - a / b) <= precision; + } +}