heatmap_data_converter.ts 11 KB

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