heatmap_data_converter.ts 11 KB

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