The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/heatmap/heatmap_data_converter.ts

447 lines
11 KiB

import _ from 'lodash';
let VALUE_INDEX = 0;
let TIME_INDEX = 1;
interface XBucket {
x: number;
buckets: any;
}
interface YBucket {
y: number;
values: number[];
}
/**
* Convert histogram represented by the list of series to heatmap object.
* @param seriesList List of time series
*/
function histogramToHeatmap(seriesList) {
let heatmap = {};
for (let i = 0; i < seriesList.length; i++) {
let series = seriesList[i];
let bound = i;
if (isNaN(bound)) {
return heatmap;
}
for (let point of series.datapoints) {
let count = point[VALUE_INDEX];
let time = point[TIME_INDEX];
if (!_.isNumber(count)) {
continue;
}
let bucket = heatmap[time];
if (!bucket) {
bucket = heatmap[time] = { x: time, buckets: {} };
}
bucket.buckets[bound] = {
y: bound,
count: count,
bounds: {
top: null,
bottom: bound,
},
values: [],
points: [],
};
}
}
return heatmap;
}
/**
* Convert buckets into linear array of "cards" - objects, represented heatmap elements.
* @param {Object} buckets
* @return {Array} Array of "card" objects
*/
function convertToCards(buckets) {
let min = 0,
max = 0;
let cards = [];
_.forEach(buckets, xBucket => {
_.forEach(xBucket.buckets, yBucket => {
let card = {
x: xBucket.x,
y: yBucket.y,
yBounds: yBucket.bounds,
values: yBucket.values,
count: yBucket.count,
};
cards.push(card);
if (cards.length === 1) {
min = yBucket.count;
max = yBucket.count;
}
min = yBucket.count < min ? yBucket.count : min;
max = yBucket.count > max ? yBucket.count : max;
});
});
let cardStats = { min, max };
return { cards, cardStats };
}
/**
* Special method for log scales. When series converted into buckets with log scale,
* for simplification, 0 values are converted into 0, not into -Infinity. On the other hand, we mean
* that all values less than series minimum, is 0 values, and we create special "minimum" bucket for
* that values (actually, there're no values less than minimum, so this bucket is empty).
* 8-16| | ** | | * | **|
* 4-8| * |* *|* |** *| * |
* 2-4| * *| | ***| |* |
* 1-2|* | | | | | This bucket contains minimum series value
* 0.5-1|____|____|____|____|____| This bucket should be displayed as 0 on graph
* 0|____|____|____|____|____| This bucket is for 0 values (should actually be -Infinity)
* So we should merge two bottom buckets into one (0-value bucket).
*
* @param {Object} buckets Heatmap buckets
* @param {Number} minValue Minimum series value
* @return {Object} Transformed buckets
*/
function mergeZeroBuckets(buckets, minValue) {
_.forEach(buckets, xBucket => {
let yBuckets = xBucket.buckets;
let emptyBucket = {
bounds: { bottom: 0, top: 0 },
values: [],
points: [],
count: 0,
};
let nullBucket = yBuckets[0] || emptyBucket;
let minBucket = yBuckets[minValue] || emptyBucket;
let newBucket = {
y: 0,
bounds: { bottom: minValue, top: minBucket.bounds.top || minValue },
values: [],
points: [],
count: 0,
};
newBucket.points = nullBucket.points.concat(minBucket.points);
newBucket.values = nullBucket.values.concat(minBucket.values);
newBucket.count = newBucket.values.length;
if (newBucket.count === 0) {
return;
}
delete yBuckets[minValue];
yBuckets[0] = newBucket;
});
return buckets;
}
/**
* Convert set of time series into heatmap buckets
* @return {Object} Heatmap object:
* {
* xBucketBound_1: {
* x: xBucketBound_1,
* buckets: {
* yBucketBound_1: {
* y: yBucketBound_1,
* bounds: {bottom, top}
* values: [val_1, val_2, ..., val_K],
* points: [[val_Y, val_X, series_name], ..., [...]],
* seriesStat: {seriesName_1: val_1, seriesName_2: val_2}
* },
* ...
* yBucketBound_M: {}
* },
* values: [val_1, val_2, ..., val_K],
* points: [
* [val_Y, val_X, series_name], (point_1)
* ...
* [...] (point_K)
* ]
* },
* xBucketBound_2: {},
* ...
* xBucketBound_N: {}
* }
*/
function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) {
let heatmap = {};
for (let series of seriesList) {
let datapoints = series.datapoints;
let seriesName = series.label;
// Slice series into X axis buckets
// | | ** | | * | **|
// | * |* *|* |** *| * |
// |** *| | ***| |* |
// |____|____|____|____|____|_
//
_.forEach(datapoints, point => {
let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize);
pushToXBuckets(heatmap, point, bucketBound, seriesName);
});
}
// Slice X axis buckets into Y (value) buckets
// | **| |2|,
// | * | --\ |1|,
// |* | --/ |1|,
// |____| |0|
//
_.forEach(heatmap, xBucket => {
if (logBase !== 1) {
xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase);
} else {
xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize);
}
});
return heatmap;
}
function pushToXBuckets(buckets, point, bucketNum, seriesName) {
let value = point[VALUE_INDEX];
if (value === null || value === undefined || isNaN(value)) {
return;
}
// Add series name to point for future identification
let point_ext = _.concat(point, seriesName);
if (buckets[bucketNum] && buckets[bucketNum].values) {
buckets[bucketNum].values.push(value);
buckets[bucketNum].points.push(point_ext);
} else {
buckets[bucketNum] = {
x: bucketNum,
values: [value],
points: [point_ext],
};
}
}
function pushToYBuckets(buckets, bucketNum, value, point, bounds) {
var count = 1;
// Use the 3rd argument as scale/count
if (point.length > 3) {
count = parseInt(point[2]);
}
if (buckets[bucketNum]) {
buckets[bucketNum].values.push(value);
buckets[bucketNum].points.push(point);
buckets[bucketNum].count += count;
} else {
buckets[bucketNum] = {
y: bucketNum,
bounds: bounds,
values: [value],
points: [point],
count: count,
};
}
}
function getValueBucketBound(value, yBucketSize, logBase) {
if (logBase === 1) {
return getBucketBound(value, yBucketSize);
} else {
return getLogScaleBucketBound(value, yBucketSize, logBase);
}
}
/**
* Find bucket for given value (for linear scale)
*/
function getBucketBounds(value, bucketSize) {
let bottom, top;
bottom = Math.floor(value / bucketSize) * bucketSize;
top = (Math.floor(value / bucketSize) + 1) * bucketSize;
return { bottom, top };
}
function getBucketBound(value, bucketSize) {
let bounds = getBucketBounds(value, bucketSize);
return bounds.bottom;
}
function convertToValueBuckets(xBucket, bucketSize) {
let values = xBucket.values;
let points = xBucket.points;
let buckets = {};
_.forEach(values, (val, index) => {
let bounds = getBucketBounds(val, bucketSize);
let bucketNum = bounds.bottom;
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
});
return buckets;
}
/**
* Find bucket for given value (for log scales)
*/
function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) {
let top, bottom;
if (value === 0) {
return { bottom: 0, top: 0 };
}
let value_log = logp(value, logBase);
let pow, powTop;
if (yBucketSplitFactor === 1 || !yBucketSplitFactor) {
pow = Math.floor(value_log);
powTop = pow + 1;
} else {
let additional_bucket_size = 1 / yBucketSplitFactor;
let additional_log = value_log - Math.floor(value_log);
additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size;
pow = Math.floor(value_log) + additional_log;
powTop = pow + additional_bucket_size;
}
bottom = Math.pow(logBase, pow);
top = Math.pow(logBase, powTop);
return { bottom, top };
}
function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) {
let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase);
return bounds.bottom;
}
function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) {
let values = xBucket.values;
let points = xBucket.points;
let buckets = {};
_.forEach(values, (val, index) => {
let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase);
let bucketNum = bounds.bottom;
pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
});
return buckets;
}
/**
* 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
* @param objB
*/
function isHeatmapDataEqual(objA: any, objB: any): boolean {
let is_eql = !emptyXOR(objA, objB);
_.forEach(objA, (xBucket: XBucket, x) => {
if (objB[x]) {
if (emptyXOR(xBucket.buckets, objB[x].buckets)) {
is_eql = false;
return false;
}
_.forEach(xBucket.buckets, (yBucket: YBucket, y) => {
if (objB[x].buckets && objB[x].buckets[y]) {
if (objB[x].buckets[y].values) {
is_eql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values));
if (!is_eql) {
return false;
} else {
return true;
}
} else {
is_eql = false;
return false;
}
} else {
is_eql = false;
return false;
}
});
if (!is_eql) {
return false;
} else {
return true;
}
} else {
is_eql = false;
return false;
}
});
return is_eql;
}
function emptyXOR(foo: any, bar: any): boolean {
return (_.isEmpty(foo) || _.isEmpty(bar)) && !(_.isEmpty(foo) && _.isEmpty(bar));
}
export {
convertToHeatMap,
histogramToHeatmap,
convertToCards,
mergeZeroBuckets,
getValueBucketBound,
isHeatmapDataEqual,
calculateBucketSize,
};