heatmap_data_converter.ts 11 KB

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