| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- import _ from 'lodash';
- const VALUE_INDEX = 0;
- const 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) {
- const heatmap = {};
- for (let i = 0; i < seriesList.length; i++) {
- const series = seriesList[i];
- const bound = i;
- if (isNaN(bound)) {
- return heatmap;
- }
- for (const point of series.datapoints) {
- const count = point[VALUE_INDEX];
- const 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;
- }
- /**
- * Sort series representing histogram by label value.
- */
- function sortSeriesByLabel(s1, s2) {
- let label1, label2;
- try {
- // fail if not integer. might happen with bad queries
- label1 = parseHistogramLabel(s1.label);
- label2 = parseHistogramLabel(s2.label);
- } catch (err) {
- console.log(err.message || err);
- return 0;
- }
- if (label1 > label2) {
- return 1;
- }
- if (label1 < label2) {
- return -1;
- }
- return 0;
- }
- function parseHistogramLabel(label: string): number {
- if (label === '+Inf' || label === 'inf') {
- return +Infinity;
- }
- const value = Number(label);
- if (isNaN(value)) {
- throw new Error(`Error parsing histogram label: ${label} is not a number`);
- }
- return value;
- }
- interface HeatmapCard {
- x: number;
- y: number;
- yBounds: {
- top: number | null;
- bottom: number | null;
- };
- values: number[];
- count: number;
- }
- interface HeatmapCardStats {
- min: number;
- max: number;
- }
- /**
- * Convert buckets into linear array of "cards" - objects, represented heatmap elements.
- * @param {Object} buckets
- * @return {Object} Array of "card" objects and stats
- */
- function convertToCards(buckets: any, hideZero = false): { cards: HeatmapCard[]; cardStats: HeatmapCardStats } {
- let min = 0,
- max = 0;
- const cards: HeatmapCard[] = [];
- _.forEach(buckets, xBucket => {
- _.forEach(xBucket.buckets, yBucket => {
- const card: HeatmapCard = {
- x: xBucket.x,
- y: yBucket.y,
- yBounds: yBucket.bounds,
- values: yBucket.values,
- count: yBucket.count,
- };
- if (!hideZero || card.count !== 0) {
- 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;
- });
- });
- const 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 => {
- const yBuckets = xBucket.buckets;
- const emptyBucket = {
- bounds: { bottom: 0, top: 0 },
- values: [],
- points: [],
- count: 0,
- };
- const nullBucket = yBuckets[0] || emptyBucket;
- const minBucket = yBuckets[minValue] || emptyBucket;
- const 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) {
- const heatmap = {};
- for (const series of seriesList) {
- const datapoints = series.datapoints;
- const seriesName = series.label;
- // Slice series into X axis buckets
- // | | ** | | * | **|
- // | * |* *|* |** *| * |
- // |** *| | ***| |* |
- // |____|____|____|____|____|_
- //
- _.forEach(datapoints, point => {
- const 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: any) => {
- if (logBase !== 1) {
- xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase);
- } else {
- xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize);
- }
- });
- return heatmap;
- }
- function pushToXBuckets(buckets, point, bucketNum, seriesName) {
- const value = point[VALUE_INDEX];
- if (value === null || value === undefined || isNaN(value)) {
- return;
- }
- // Add series name to point for future identification
- const pointExt = _.concat(point, seriesName);
- if (buckets[bucketNum] && buckets[bucketNum].values) {
- buckets[bucketNum].values.push(value);
- buckets[bucketNum].points.push(pointExt);
- } else {
- buckets[bucketNum] = {
- x: bucketNum,
- values: [value],
- points: [pointExt],
- };
- }
- }
- function pushToYBuckets(buckets, bucketNum, value, point, bounds) {
- let count = 1;
- // Use the 3rd argument as scale/count
- if (point.length > 3) {
- count = parseInt(point[2], 10);
- }
- 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) {
- const bounds = getBucketBounds(value, bucketSize);
- return bounds.bottom;
- }
- function convertToValueBuckets(xBucket, bucketSize) {
- const values = xBucket.values;
- const points = xBucket.points;
- const buckets = {};
- _.forEach(values, (val, index) => {
- const bounds = getBucketBounds(val, bucketSize);
- const 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 };
- }
- const valueLog = logp(value, logBase);
- let pow, powTop;
- if (yBucketSplitFactor === 1 || !yBucketSplitFactor) {
- pow = Math.floor(valueLog);
- powTop = pow + 1;
- } else {
- const additionalBucketSize = 1 / yBucketSplitFactor;
- let additionalLog = valueLog - Math.floor(valueLog);
- additionalLog = Math.floor(additionalLog / additionalBucketSize) * additionalBucketSize;
- pow = Math.floor(valueLog) + additionalLog;
- powTop = pow + additionalBucketSize;
- }
- bottom = Math.pow(logBase, pow);
- top = Math.pow(logBase, powTop);
- return { bottom, top };
- }
- function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) {
- const bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase);
- return bounds.bottom;
- }
- function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) {
- const values = xBucket.values;
- const points = xBucket.points;
- const buckets = {};
- _.forEach(values, (val, index) => {
- const bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase);
- const 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++) {
- const 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
- const 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 isEql = !emptyXOR(objA, objB);
- _.forEach(objA, (xBucket: XBucket, x) => {
- if (objB[x]) {
- if (emptyXOR(xBucket.buckets, objB[x].buckets)) {
- isEql = false;
- return false;
- }
- _.forEach(xBucket.buckets, (yBucket: YBucket, y) => {
- if (objB[x].buckets && objB[x].buckets[y]) {
- if (objB[x].buckets[y].values) {
- isEql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values));
- if (!isEql) {
- return false;
- } else {
- return true;
- }
- } else {
- isEql = false;
- return false;
- }
- } else {
- isEql = false;
- return false;
- }
- });
- if (!isEql) {
- return false;
- } else {
- return true;
- }
- } else {
- isEql = false;
- return false;
- }
- });
- return isEql;
- }
- function emptyXOR(foo: any, bar: any): boolean {
- return (_.isEmpty(foo) || _.isEmpty(bar)) && !(_.isEmpty(foo) && _.isEmpty(bar));
- }
- export {
- convertToHeatMap,
- histogramToHeatmap,
- convertToCards,
- mergeZeroBuckets,
- getValueBucketBound,
- isHeatmapDataEqual,
- calculateBucketSize,
- sortSeriesByLabel,
- };
|