heatmap_data_converter.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. ///<reference path="../../../headers/common.d.ts" />
  2. import _ from 'lodash';
  3. import TimeSeries from 'app/core/time_series2';
  4. let VALUE_INDEX = 0;
  5. let TIME_INDEX = 1;
  6. interface XBucket {
  7. x: number;
  8. buckets: any;
  9. }
  10. interface YBucket {
  11. y: number;
  12. values: number[];
  13. }
  14. function elasticHistogramToHeatmap(series) {
  15. let seriesBuckets = _.map(series, (s: TimeSeries) => {
  16. return convertEsSeriesToHeatmap(s);
  17. });
  18. let buckets = mergeBuckets(seriesBuckets);
  19. return buckets;
  20. }
  21. function convertEsSeriesToHeatmap(series: TimeSeries, saveZeroCounts = false) {
  22. let xBuckets: XBucket[] = [];
  23. _.forEach(series.datapoints, point => {
  24. let bound = series.alias;
  25. let count = point[VALUE_INDEX];
  26. if (!count) {
  27. return;
  28. }
  29. let values = new Array(Math.round(count));
  30. values.fill(Number(bound));
  31. let valueBuckets = {};
  32. valueBuckets[bound] = {
  33. y: Number(bound),
  34. values: values
  35. };
  36. let xBucket: XBucket = {
  37. x: point[TIME_INDEX],
  38. buckets: valueBuckets
  39. };
  40. // Don't push buckets with 0 count until saveZeroCounts flag is set
  41. if (count !== 0 || (count === 0 && saveZeroCounts)) {
  42. xBuckets.push(xBucket);
  43. }
  44. });
  45. let heatmap: any = {};
  46. _.forEach(xBuckets, (bucket: XBucket) => {
  47. heatmap[bucket.x] = bucket;
  48. });
  49. return heatmap;
  50. }
  51. /**
  52. * Convert set of time series into heatmap buckets
  53. * @return {Object} Heatmap object:
  54. * {
  55. * xBucketBound_1: {
  56. * x: xBucketBound_1,
  57. * buckets: {
  58. * yBucketBound_1: {
  59. * y: yBucketBound_1,
  60. * bounds: {bottom, top}
  61. * values: [val_1, val_2, ..., val_K],
  62. * points: [[val_Y, val_X, series_name], ..., [...]],
  63. * seriesStat: {seriesName_1: val_1, seriesName_2: val_2}
  64. * },
  65. * ...
  66. * yBucketBound_M: {}
  67. * },
  68. * values: [val_1, val_2, ..., val_K],
  69. * points: [
  70. * [val_Y, val_X, series_name], (point_1)
  71. * ...
  72. * [...] (point_K)
  73. * ]
  74. * },
  75. * xBucketBound_2: {},
  76. * ...
  77. * xBucketBound_N: {}
  78. * }
  79. */
  80. function convertToHeatMap(series, yBucketSize, xBucketSize, logBase) {
  81. let seriesBuckets = _.map(series, s => {
  82. return seriesToHeatMap(s, yBucketSize, xBucketSize, logBase);
  83. });
  84. let buckets = mergeBuckets(seriesBuckets);
  85. return buckets;
  86. }
  87. /**
  88. * Convert buckets into linear array of "cards" - objects, represented heatmap elements.
  89. * @param {Object} buckets
  90. * @return {Array} Array of "card" objects
  91. */
  92. function convertToCards(buckets) {
  93. let cards = [];
  94. _.forEach(buckets, xBucket => {
  95. _.forEach(xBucket.buckets, (yBucket, key) => {
  96. if (yBucket.values.length) {
  97. let card = {
  98. x: Number(xBucket.x),
  99. y: Number(key),
  100. yBounds: yBucket.bounds,
  101. values: yBucket.values,
  102. seriesStat: getSeriesStat(yBucket.points)
  103. };
  104. cards.push(card);
  105. }
  106. });
  107. });
  108. return cards;
  109. }
  110. /**
  111. * Special method for log scales. When series converted into buckets with log scale,
  112. * for simplification, 0 values are converted into 0, not into -Infinity. On the other hand, we mean
  113. * that all values less than series minimum, is 0 values, and we create special "minimum" bucket for
  114. * that values (actually, there're no values less than minimum, so this bucket is empty).
  115. * 8-16| | ** | | * | **|
  116. * 4-8| * |* *|* |** *| * |
  117. * 2-4| * *| | ***| |* |
  118. * 1-2|* | | | | | This bucket contains minimum series value
  119. * 0.5-1|____|____|____|____|____| This bucket should be displayed as 0 on graph
  120. * 0|____|____|____|____|____| This bucket is for 0 values (should actually be -Infinity)
  121. * So we should merge two bottom buckets into one (0-value bucket).
  122. *
  123. * @param {Object} buckets Heatmap buckets
  124. * @param {Number} minValue Minimum series value
  125. * @return {Object} Transformed buckets
  126. */
  127. function mergeZeroBuckets(buckets, minValue) {
  128. _.forEach(buckets, xBucket => {
  129. let yBuckets = xBucket.buckets;
  130. let emptyBucket = {
  131. bounds: {bottom: 0, top: 0},
  132. values: [],
  133. points: []
  134. };
  135. let nullBucket = yBuckets[0] || emptyBucket;
  136. let minBucket = yBuckets[minValue] || emptyBucket;
  137. let newBucket = {
  138. y: 0,
  139. bounds: {bottom: minValue, top: minBucket.bounds.top || minValue},
  140. values: [],
  141. points: []
  142. };
  143. if (nullBucket.values) {
  144. newBucket.values = nullBucket.values.concat(minBucket.values);
  145. }
  146. if (nullBucket.points) {
  147. newBucket.points = nullBucket.points.concat(minBucket.points);
  148. }
  149. let newYBuckets = {};
  150. _.forEach(yBuckets, (bucket, bound) => {
  151. bound = Number(bound);
  152. if (bound !== 0 && bound !== minValue) {
  153. newYBuckets[bound] = bucket;
  154. }
  155. });
  156. newYBuckets[0] = newBucket;
  157. xBucket.buckets = newYBuckets;
  158. });
  159. return buckets;
  160. }
  161. /**
  162. * Remove 0 values from heatmap buckets.
  163. */
  164. function removeZeroBuckets(buckets) {
  165. _.forEach(buckets, xBucket => {
  166. let yBuckets = xBucket.buckets;
  167. let newYBuckets = {};
  168. _.forEach(yBuckets, (bucket, bound) => {
  169. if (bucket.y !== 0) {
  170. newYBuckets[bound] = bucket;
  171. }
  172. });
  173. xBucket.buckets = newYBuckets;
  174. });
  175. return buckets;
  176. }
  177. /**
  178. * Count values number for each timeseries in given bucket
  179. * @param {Array} points Bucket's datapoints with series name ([val, ts, series_name])
  180. * @return {Object} seriesStat: {seriesName_1: val_1, seriesName_2: val_2}
  181. */
  182. function getSeriesStat(points) {
  183. return _.countBy(points, p => p[2]);
  184. }
  185. /**
  186. * Convert individual series to heatmap buckets
  187. */
  188. function seriesToHeatMap(series, yBucketSize, xBucketSize, logBase = 1) {
  189. let datapoints = series.datapoints;
  190. let seriesName = series.label;
  191. let xBuckets = {};
  192. // Slice series into X axis buckets
  193. // | | ** | | * | **|
  194. // | * |* *|* |** *| * |
  195. // |** *| | ***| |* |
  196. // |____|____|____|____|____|_
  197. //
  198. _.forEach(datapoints, point => {
  199. let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize);
  200. pushToXBuckets(xBuckets, point, bucketBound, seriesName);
  201. });
  202. // Slice X axis buckets into Y (value) buckets
  203. // | **| |2|,
  204. // | * | --\ |1|,
  205. // |* | --/ |1|,
  206. // |____| |0|
  207. //
  208. _.forEach(xBuckets, xBucket => {
  209. if (logBase !== 1) {
  210. xBucket.buckets = convertToLogScaleValueBuckets(xBucket, yBucketSize, logBase);
  211. } else {
  212. xBucket.buckets = convertToValueBuckets(xBucket, yBucketSize);
  213. }
  214. });
  215. return xBuckets;
  216. }
  217. function pushToXBuckets(buckets, point, bucketNum, seriesName) {
  218. let value = point[VALUE_INDEX];
  219. if (value === null || value === undefined || isNaN(value)) { return; }
  220. // Add series name to point for future identification
  221. point.push(seriesName);
  222. if (buckets[bucketNum] && buckets[bucketNum].values) {
  223. buckets[bucketNum].values.push(value);
  224. buckets[bucketNum].points.push(point);
  225. } else {
  226. buckets[bucketNum] = {
  227. x: bucketNum,
  228. values: [value],
  229. points: [point]
  230. };
  231. }
  232. }
  233. function pushToYBuckets(buckets, bucketNum, value, point, bounds) {
  234. if (buckets[bucketNum]) {
  235. buckets[bucketNum].values.push(value);
  236. buckets[bucketNum].points.push(point);
  237. } else {
  238. buckets[bucketNum] = {
  239. y: bucketNum,
  240. bounds: bounds,
  241. values: [value],
  242. points: [point]
  243. };
  244. }
  245. }
  246. function getValueBucketBound(value, yBucketSize, logBase) {
  247. if (logBase === 1) {
  248. return getBucketBound(value, yBucketSize);
  249. } else {
  250. return getLogScaleBucketBound(value, yBucketSize, logBase);
  251. }
  252. }
  253. /**
  254. * Find bucket for given value (for linear scale)
  255. */
  256. function getBucketBounds(value, bucketSize) {
  257. let bottom, top;
  258. bottom = Math.floor(value / bucketSize) * bucketSize;
  259. top = (Math.floor(value / bucketSize) + 1) * bucketSize;
  260. return {bottom, top};
  261. }
  262. function getBucketBound(value, bucketSize) {
  263. let bounds = getBucketBounds(value, bucketSize);
  264. return bounds.bottom;
  265. }
  266. function convertToValueBuckets(xBucket, bucketSize) {
  267. let values = xBucket.values;
  268. let points = xBucket.points;
  269. let buckets = {};
  270. _.forEach(values, (val, index) => {
  271. let bounds = getBucketBounds(val, bucketSize);
  272. let bucketNum = bounds.bottom;
  273. pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
  274. });
  275. return buckets;
  276. }
  277. /**
  278. * Find bucket for given value (for log scales)
  279. */
  280. function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) {
  281. let top, bottom;
  282. if (value === 0) {
  283. return {bottom: 0, top: 0};
  284. }
  285. let value_log = logp(value, logBase);
  286. let pow, powTop;
  287. if (yBucketSplitFactor === 1 || !yBucketSplitFactor) {
  288. pow = Math.floor(value_log);
  289. powTop = pow + 1;
  290. } else {
  291. let additional_bucket_size = 1 / yBucketSplitFactor;
  292. let additional_log = value_log - Math.floor(value_log);
  293. additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size;
  294. pow = Math.floor(value_log) + additional_log;
  295. powTop = pow + additional_bucket_size;
  296. }
  297. bottom = Math.pow(logBase, pow);
  298. top = Math.pow(logBase, powTop);
  299. return {bottom, top};
  300. }
  301. function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) {
  302. let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase);
  303. return bounds.bottom;
  304. }
  305. function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) {
  306. let values = xBucket.values;
  307. let points = xBucket.points;
  308. let buckets = {};
  309. _.forEach(values, (val, index) => {
  310. let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase);
  311. let bucketNum = bounds.bottom;
  312. pushToYBuckets(buckets, bucketNum, val, points[index], bounds);
  313. });
  314. return buckets;
  315. }
  316. /**
  317. * Merge individual buckets for all series into one
  318. * @param {Array} seriesBuckets Array of series buckets
  319. * @return {Object} Merged buckets.
  320. */
  321. function mergeBuckets(seriesBuckets) {
  322. let mergedBuckets: any = {};
  323. _.forEach(seriesBuckets, (seriesBucket, index) => {
  324. if (index === 0) {
  325. mergedBuckets = seriesBucket;
  326. } else {
  327. _.forEach(seriesBucket, (xBucket, xBound) => {
  328. if (mergedBuckets[xBound]) {
  329. if (xBucket.points) {
  330. mergedBuckets[xBound].points = xBucket.points.concat(mergedBuckets[xBound].points);
  331. }
  332. if (xBucket.values) {
  333. mergedBuckets[xBound].values = xBucket.values.concat(mergedBuckets[xBound].values);
  334. }
  335. _.forEach(xBucket.buckets, (yBucket, yBound) => {
  336. let bucket = mergedBuckets[xBound].buckets[yBound];
  337. if (bucket && bucket.values) {
  338. mergedBuckets[xBound].buckets[yBound].values = bucket.values.concat(yBucket.values);
  339. if (bucket.points) {
  340. mergedBuckets[xBound].buckets[yBound].points = bucket.points.concat(yBucket.points);
  341. }
  342. } else {
  343. mergedBuckets[xBound].buckets[yBound] = yBucket;
  344. }
  345. let points = mergedBuckets[xBound].buckets[yBound].points;
  346. if (points) {
  347. mergedBuckets[xBound].buckets[yBound].seriesStat = getSeriesStat(points);
  348. }
  349. });
  350. } else {
  351. mergedBuckets[xBound] = xBucket;
  352. }
  353. });
  354. }
  355. });
  356. return mergedBuckets;
  357. }
  358. // Get minimum non zero value.
  359. function getMinLog(series) {
  360. let values = _.compact(_.map(series.datapoints, p => p[0]));
  361. return _.min(values);
  362. }
  363. /**
  364. * Logarithm for custom base
  365. * @param value
  366. * @param base logarithm base
  367. */
  368. function logp(value, base) {
  369. return Math.log(value) / Math.log(base);
  370. }
  371. /**
  372. * Calculate size of Y bucket from given buckets bounds.
  373. * @param bounds Array of Y buckets bounds
  374. * @param logBase Logarithm base
  375. */
  376. function calculateBucketSize(bounds: number[], logBase = 1): number {
  377. let bucketSize = Infinity;
  378. if (bounds.length === 0) {
  379. return 0;
  380. } else if (bounds.length === 1) {
  381. return bounds[0];
  382. } else {
  383. bounds = _.sortBy(bounds);
  384. for (let i = 1; i < bounds.length; i++) {
  385. let distance = getDistance(bounds[i], bounds[i - 1], logBase);
  386. bucketSize = distance < bucketSize ? distance : bucketSize;
  387. }
  388. }
  389. return bucketSize;
  390. }
  391. /**
  392. * Calculate distance between two numbers in given scale (linear or logarithmic).
  393. * @param a
  394. * @param b
  395. * @param logBase
  396. */
  397. function getDistance(a: number, b: number, logBase = 1): number {
  398. if (logBase === 1) {
  399. // Linear distance
  400. return Math.abs(b - a);
  401. } else {
  402. // logarithmic distance
  403. let ratio = Math.max(a, b) / Math.min(a, b);
  404. return logp(ratio, logBase);
  405. }
  406. }
  407. /**
  408. * Compare two heatmap data objects
  409. * @param objA
  410. * @param objB
  411. */
  412. function isHeatmapDataEqual(objA: any, objB: any): boolean {
  413. let is_eql = !emptyXOR(objA, objB);
  414. _.forEach(objA, (xBucket: XBucket, x) => {
  415. if (objB[x]) {
  416. if (emptyXOR(xBucket.buckets, objB[x].buckets)) {
  417. is_eql = false;
  418. return false;
  419. }
  420. _.forEach(xBucket.buckets, (yBucket: YBucket, y) => {
  421. if (objB[x].buckets && objB[x].buckets[y]) {
  422. if (objB[x].buckets[y].values) {
  423. is_eql = _.isEqual(_.sortBy(yBucket.values), _.sortBy(objB[x].buckets[y].values));
  424. if (!is_eql) {
  425. return false;
  426. }
  427. } else {
  428. is_eql = false;
  429. return false;
  430. }
  431. } else {
  432. is_eql = false;
  433. return false;
  434. }
  435. });
  436. if (!is_eql) {
  437. return false;
  438. }
  439. } else {
  440. is_eql = false;
  441. return false;
  442. }
  443. });
  444. return is_eql;
  445. }
  446. function emptyXOR(foo: any, bar: any): boolean {
  447. return (_.isEmpty(foo) || _.isEmpty(bar)) && !(_.isEmpty(foo) && _.isEmpty(bar));
  448. }
  449. export {
  450. convertToHeatMap,
  451. elasticHistogramToHeatmap,
  452. convertToCards,
  453. removeZeroBuckets,
  454. mergeZeroBuckets,
  455. getMinLog,
  456. getValueBucketBound,
  457. isHeatmapDataEqual,
  458. calculateBucketSize
  459. };