PostProcessMeasuresAsSeries.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. 'use strict';
  2. /*
  3. *+------------------------------------------------------------------------+
  4. *| Licensed Materials - Property of IBM
  5. *| IBM Cognos Products: BI Dashboard
  6. *| (C) Copyright IBM Corp. 2017, 2019
  7. *|
  8. *| US Government Users Restricted Rights - Use, duplication or disclosure
  9. *| restricted by GSA ADP Schedule Contract with IBM Corp.
  10. *+------------------------------------------------------------------------+
  11. */
  12. define(['jquery', 'underscore', './VisQueryPostProcessor', '../../nls/StringResources', '../QueryResultDataUtils', '../../../../apiHelpers/SlotAPIHelper'], function ($, _, VisQueryPostprocessor, StringResources, QueryResultDataUtils, SlotAPIHelper) {
  13. 'use strict';
  14. /**
  15. * This Class does Query result response post-processing such as processing the original query result
  16. * into a data re-structure to support multiple measures as series.
  17. **/
  18. var PostprocessorClass = VisQueryPostprocessor.extend({
  19. /**
  20. * @Constructor
  21. * @param {Object} options
  22. */
  23. init: function init(options) {
  24. PostprocessorClass.inherited('init', this, arguments);
  25. this.mappingAPI = options.mappingAPI;
  26. },
  27. /**
  28. * Process Query result based on the passed options to constructor.
  29. * For instance, limit the rows of datapoints so that in the response, for
  30. * specified dataitem, 'product', there will be only three tuples returned
  31. *
  32. * @return {QueryResultData}
  33. */
  34. _processData: function _processData() {
  35. if (this._queryResultData && this._queryResultData.data && this._canProcess()) {
  36. var result = this._categorizeDataItems();
  37. // post-process the data items
  38. this._processDataItems(result);
  39. // post-process the data points
  40. this._processDataPoints(result);
  41. }
  42. //Process Done
  43. return this._queryResultData;
  44. },
  45. /**
  46. * Determine whether multiple measure series is supported
  47. */
  48. _canProcess: function _canProcess() {
  49. var slotAPIs = this.mappingAPI.getSlotAPIs();
  50. var hasMultiMeasuresSeries = false,
  51. hasMultiMeasuresVal = false;
  52. _.each(slotAPIs, function (slotAPI) {
  53. hasMultiMeasuresSeries = hasMultiMeasuresSeries || slotAPI.isMultiMeasuresSeries();
  54. hasMultiMeasuresVal = hasMultiMeasuresVal || slotAPI.isMultiMeasuresValue();
  55. });
  56. return hasMultiMeasuresSeries && hasMultiMeasuresVal;
  57. },
  58. /**
  59. * Get the mapped projectionId for each dataItem.
  60. *
  61. * note: the logic here should be the same in visQueryBuilder.js:_slotToProjections()
  62. */
  63. _getQueryProjectionId: function _getQueryProjectionId(slotAPI, dataItemIdx) {
  64. // We don't project MultiMeasureSeries dataItem
  65. if (slotAPI.isMultiMeasuresSeriesOrValue(dataItemIdx)) return null;
  66. var validDataItemIds = _.filter(slotAPI.getDataItemRefs(), function (dataItemUniqueId, ind) {
  67. return !slotAPI.isMultiMeasuresSeriesOrValue(ind);
  68. });
  69. if (slotAPI.isStacked() && validDataItemIds.length > 1) {
  70. return slotAPI.getId();
  71. }
  72. return slotAPI.getDataItemAPI(dataItemIdx).getUniqueId();
  73. },
  74. /**
  75. * Categorize the existing data items by category and ordinals
  76. * Keep track of data items original index and also the slot Id it which the data item is mapped.
  77. * The original index later allows the data points (prior to restructure) to access the mapping slot and data item.
  78. *
  79. * Despite the result data being the original data, the slot mapping already includes the multi-measures series mapping.
  80. * The slot mapping for mult-measures series needs to be ignored at this stage in order to match with the original data.
  81. */
  82. _categorizeDataItems: function _categorizeDataItems() {
  83. var result = {
  84. cats: [],
  85. ords: [],
  86. commonIndices: {},
  87. mappedIndices: {},
  88. extraDataItems: []
  89. };
  90. var resultQueryDataItems = this._queryResultData.dataItems;
  91. var allProjectionIds = _.map(resultQueryDataItems, function (dataItem) {
  92. return dataItem.itemClass.id;
  93. });
  94. var mappedProjectionIds = [];
  95. var slotAPIs = this.mappingAPI.getSlotAPIs();
  96. for (var i = 0; i < slotAPIs.length; i++) {
  97. var slot = slotAPIs[i];
  98. for (var dataItemIdx = 0; dataItemIdx < slot.getDataItemAPIs().length; dataItemIdx++) {
  99. var queryProjectionId = this._getQueryProjectionId(slot, dataItemIdx);
  100. if (queryProjectionId) {
  101. var idx = allProjectionIds.indexOf(queryProjectionId);
  102. var newEntry = {
  103. item: idx !== -1 ? resultQueryDataItems[idx] : undefined,
  104. slotId: slot.getId(),
  105. index: idx !== -1 ? idx : undefined
  106. };
  107. if (newEntry.index !== undefined) {
  108. result.mappedIndices[newEntry.index] = true;
  109. if (slot.getFinalSlotType() === 'ordinal' && !slot.getDefinition().multiMeasure) {
  110. result.commonIndices[newEntry.index] = true;
  111. }
  112. }
  113. (slot.getFinalSlotType() === 'category' ? result.cats : result.ords).push(newEntry);
  114. mappedProjectionIds.push(queryProjectionId);
  115. }
  116. }
  117. }
  118. var extraDataItemIds = _.difference(allProjectionIds, mappedProjectionIds);
  119. resultQueryDataItems.forEach(function (dataItem, index) {
  120. if (extraDataItemIds.indexOf(dataItem.itemClass.id) !== -1) {
  121. result.commonIndices[index] = true;
  122. result.extraDataItems.push({ dataItem: dataItem });
  123. }
  124. });
  125. return result;
  126. },
  127. /**
  128. * Process the data items
  129. * Consolidate the multiple measures to one measure data item and a extra series to indicate the measure
  130. */
  131. _processDataItems: function _processDataItems(dataItems) {
  132. var resultData = this._queryResultData;
  133. var slotAPIs = this.mappingAPI.getSlotAPIs();
  134. // rebuild the dataItems from scratch!
  135. resultData.dataItems.length = 0;
  136. // maintain the order based on slots
  137. _.each(slotAPIs, function (slotAPI) {
  138. var dataItem;
  139. var isCategory = slotAPI.getFinalSlotType() === 'category';
  140. if (isCategory) {
  141. if (slotAPI.isMultiMeasuresSeries()) {
  142. var measures = _.map(dataItems.ords, function (ordinal) {
  143. if (ordinal && ordinal.item && ordinal.item.itemClass && ordinal.slotId === 'values') {
  144. return ordinal.item.itemClass.h[0];
  145. }
  146. return undefined;
  147. });
  148. measures = _.compact(measures);
  149. // apply the multiple measures as a series
  150. dataItem = this._getSeriesDataItemforMultipleMeasures(dataItems.cats, measures, slotAPI);
  151. } else {
  152. dataItem = this._getDataItem(dataItems.cats, slotAPI);
  153. }
  154. } else if (slotAPI.isMultiMeasuresValue()) {
  155. // apply the value for all measures
  156. dataItem = this._getValueDataItemforMultipleMeasures(slotAPI);
  157. } else {
  158. dataItem = this._getDataItem(dataItems.ords, slotAPI);
  159. }
  160. if (dataItem) {
  161. resultData.dataItems.push(dataItem);
  162. }
  163. }.bind(this));
  164. dataItems.extraDataItems.forEach(function (extraDataItem) {
  165. extraDataItem.index = resultData.dataItems.length;
  166. resultData.dataItems.push(extraDataItem.dataItem);
  167. });
  168. },
  169. /**
  170. * Find the data item mapped to a slotAPI
  171. */
  172. _getDataItem: function _getDataItem(items, slotAPI) {
  173. var slotItem = _.find(items, function (item) {
  174. return item.slotId === slotAPI.getId();
  175. });
  176. return slotItem ? slotItem.item : null;
  177. },
  178. /**
  179. * Collect the unique values from a category data item
  180. */
  181. _getCategoryDataItemValues: function _getCategoryDataItemValues(categoryItem, stackIndex) {
  182. if (stackIndex > -1) {
  183. var stackValues = {};
  184. // collect the unique values
  185. _.each(categoryItem.items, function (item) {
  186. stackValues[item.t[stackIndex].u] = item.t[stackIndex];
  187. });
  188. var keys = Object.keys(stackValues);
  189. return _.map(keys, function (key) {
  190. return stackValues[key];
  191. });
  192. }
  193. return [];
  194. },
  195. /**
  196. * Create a category data item for the multiple measures series
  197. */
  198. _getSeriesDataItemforMultipleMeasures: function _getSeriesDataItemforMultipleMeasures(cats, measures, slotAPI) {
  199. return {
  200. itemClass: this._getSeriesDataItemClass(cats, slotAPI),
  201. items: this._getSeriesTupleItems(cats, measures, slotAPI)
  202. };
  203. },
  204. /**
  205. * Collect all data item Ids from a given slot
  206. */
  207. _getSlotDataItemIds: function _getSlotDataItemIds(slotAPI) {
  208. return _.map(slotAPI.getDataItemAPIs(), function (dataItemAPI) {
  209. return dataItemAPI.getItemId();
  210. });
  211. },
  212. /**
  213. * Collect all series data item class
  214. */
  215. _getSeriesDataItemClass: function _getSeriesDataItemClass(cats, slotAPI) {
  216. var ids = this._getSlotDataItemIds(slotAPI);
  217. var stackedItem = this._getDataItem(cats, slotAPI);
  218. return {
  219. id: slotAPI.getId(),
  220. h: _.map(ids, function (id, index) {
  221. if (slotAPI.isMultiMeasuresSeriesOrValue(index)) {
  222. return {
  223. u: SlotAPIHelper.MULTI_MEASURES_SERIES,
  224. d: StringResources.get('MeasuresCaption')
  225. };
  226. } else {
  227. if (stackedItem && stackedItem.itemClass) {
  228. return _.find(stackedItem.itemClass.h, function (header) {
  229. return header.u === id;
  230. });
  231. }
  232. return undefined;
  233. }
  234. })
  235. };
  236. },
  237. /**
  238. * Collect all series data item tuple items
  239. */
  240. _getSeriesTupleItems: function _getSeriesTupleItems(cats, measures, slotAPI) {
  241. var rows = [];
  242. var baseRows = [];
  243. var catItem = this._getDataItem(cats, slotAPI);
  244. var stackIds = catItem ? _.map(catItem.itemClass.h, function (header) {
  245. return header.u;
  246. }) : [];
  247. var ids = this._getSlotDataItemIds(slotAPI);
  248. _.each(ids, function (id, index) {
  249. var _this = this;
  250. var isMultiMeasureSeries = slotAPI.isMultiMeasuresSeriesOrValue(index);
  251. var values = isMultiMeasureSeries ? measures : this._getCategoryDataItemValues(catItem, stackIds.indexOf(id));
  252. //clean up rows and baseRows, so that no defect occurs in the loop with more than 2 ids.
  253. if (rows.length != 0) {
  254. baseRows = rows;
  255. }
  256. rows = [];
  257. if (baseRows.length > 0) {
  258. if (values.length > 0) {
  259. // Should preserve the drop order in multi measure series slot so iterate from baseRows to values
  260. _.each(baseRows, function (row) {
  261. // duplicate the values tuple
  262. var clone = values.slice();
  263. _.each(clone, function (value, valueIndex) {
  264. var newRow = valueIndex === 0 ? row : $.extend(true, {}, row);
  265. newRow.t[index] = isMultiMeasureSeries ? {
  266. u: _this._getMultiMeasureMun(valueIndex),
  267. d: value.d,
  268. aggregate: value.aggregate
  269. } : value;
  270. rows.push(newRow);
  271. });
  272. });
  273. } else {
  274. // since there are no data to duplicate, simply update the base rows with null data
  275. _.each(baseRows, function (row) {
  276. row.t[index] = {
  277. u: null,
  278. d: null
  279. };
  280. });
  281. }
  282. } else {
  283. // prepare the base tuple item rows
  284. baseRows.push.apply(baseRows, _.map(values, function (value, index) {
  285. return {
  286. t: [isMultiMeasureSeries ? {
  287. u: value && value.u ? _this._getMultiMeasureMun(index) : 'undefined',
  288. d: value && value.d ? value.d : 'undefined',
  289. aggregate: value.aggregate
  290. } : value]
  291. };
  292. }));
  293. }
  294. }.bind(this));
  295. // finish with the base rows if not expecting a category (i.e. not nested)
  296. if (!catItem && rows.length === 0) {
  297. rows.push.apply(rows, baseRows.slice());
  298. }
  299. return rows;
  300. },
  301. /**
  302. * Create a measure data item for the multiple measure values
  303. */
  304. _getValueDataItemforMultipleMeasures: function _getValueDataItemforMultipleMeasures(slotAPI) {
  305. return {
  306. itemClass: {
  307. id: slotAPI.getId(),
  308. h: [{
  309. u: SlotAPIHelper.MULTI_MEASURES_VALUE,
  310. d: StringResources.get('ValuesCaption')
  311. }]
  312. }
  313. };
  314. },
  315. /**
  316. * Process all data point to restructure the data for multiple measures value and series
  317. */
  318. _processDataPoints: function _processDataPoints(dataItems) {
  319. // prepare the new index for each slot
  320. var newIndices = this._allocateDataPointIndices(dataItems.extraDataItems);
  321. // restructured data
  322. var newData = [];
  323. var resultData = this._queryResultData;
  324. var multiMeasureSeriesId = this._getMultiMeasureSeriesId();
  325. _.each(resultData.data, function (datapoint) {
  326. this._restructureDataPoint(dataItems, datapoint, newIndices, newData, multiMeasureSeriesId);
  327. }.bind(this));
  328. resultData.data = newData;
  329. },
  330. /**
  331. * Allocate the datapoint index for each data item.
  332. * Since the multiple measures are wrapped into a single measure with an extra series for the measures,
  333. * None of the slots would have multiple data items.
  334. *
  335. * ie. category, multimeasure_series, category(series), value
  336. * indices: {
  337. * seriesIndex: 1,
  338. * valueIndex: 3,
  339. * catIndices: [0, 2]
  340. * }
  341. */
  342. _allocateDataPointIndices: function _allocateDataPointIndices(extraDataItems) {
  343. var catIndex = 0;
  344. var commonValueIndex = 0;
  345. /**
  346. * [todo] livewidget-cleanup: need to be refactored to use the new api.
  347. */
  348. var slotAPIs = this.mappingAPI.getSlotAPIs();
  349. var indices = {
  350. seriesIndex: -1,
  351. valueIndex: -1,
  352. commonIndices: [],
  353. catIndices: []
  354. };
  355. _.each(slotAPIs, function (slotAPI, slotIndex) {
  356. var isCategory = slotAPI.getFinalSlotType() === 'category';
  357. if (isCategory) {
  358. if (slotAPI.isMultiMeasuresSeries()) {
  359. indices.seriesIndex = slotIndex;
  360. } else {
  361. indices.catIndices[catIndex++] = slotIndex;
  362. }
  363. } else if (slotAPI.isMultiMeasuresValue()) {
  364. indices.valueIndex = slotIndex;
  365. } else {
  366. indices.commonIndices[commonValueIndex++] = slotIndex;
  367. }
  368. }.bind(this));
  369. extraDataItems.forEach(function (dataItem) {
  370. return indices.commonIndices[commonValueIndex++] = dataItem.index;
  371. });
  372. return indices;
  373. },
  374. /**
  375. * Restructure a single data point.
  376. * Consolidate multiple measures to one value
  377. * Categorize the measures to one category
  378. */
  379. _restructureDataPoint: function _restructureDataPoint(dataItems, datapoint, indices, newData, multiMeasureSeriesId) {
  380. var _this2 = this;
  381. var catValueIndex = 0;
  382. var commonValueIndex = 0;
  383. var point = [];
  384. var ordValues = []; // has to be in order
  385. var catItemInSeries = void 0;
  386. var seriesIndexValue = 0;
  387. var slotAPIs = this.mappingAPI.getSlotAPIs();
  388. // Prepare the common portion of the data point
  389. _.each(datapoint.pt, function (value, valueIndex) {
  390. var category = _.find(dataItems.cats, function (cat) {
  391. return cat.index === valueIndex;
  392. });
  393. if (category) {
  394. var slotAPI = _.find(slotAPIs, function (slotAPI) {
  395. return slotAPI.getId() === category.slotId;
  396. });
  397. if (slotAPI.isMultiMeasuresSeries()) {
  398. catItemInSeries = category.item;
  399. seriesIndexValue = value;
  400. } else {
  401. point[indices.catIndices[catValueIndex++]] = value;
  402. }
  403. } else if (dataItems.commonIndices[valueIndex]) {
  404. point[indices.commonIndices[commonValueIndex++]] = value;
  405. } else if (dataItems.mappedIndices[valueIndex]) {
  406. // Keep track of the remaining ordinal values (as long as they're 'valid' - mapped to a slot)
  407. ordValues.push(value);
  408. }
  409. });
  410. // Duplicate the data point by each measure
  411. var processedIndices = [];
  412. _.each(ordValues, function (ordValue, indexValue) {
  413. var pt = indexValue === 0 ? point : point.slice();
  414. pt[indices.seriesIndex] = _this2._getSeriesIndex(dataItems, indexValue, catItemInSeries, seriesIndexValue, processedIndices, multiMeasureSeriesId);
  415. pt[indices.valueIndex] = ordValue;
  416. newData.push({ pt: pt });
  417. processedIndices.push(indexValue);
  418. });
  419. },
  420. _getSeriesIndex: function _getSeriesIndex(dataItems, ordIndex, catItemInSeries, originalIndex, processedIndices, multiMeasureSeriesId) {
  421. // The multiSeriesResultItem has tuples built from _getSeriesTupleItems. It's a cross joined tuple set of the multi measure series
  422. var multiSeriesResultItem = this._queryResultData && this._queryResultData.dataItems && this._queryResultData.dataItems[QueryResultDataUtils.getDataItemIndex(this._queryResultData, multiMeasureSeriesId)];
  423. // The target tuple to search in multiSeriesResultItem and return its index
  424. var measureMun = this._getMultiMeasureMun(ordIndex);
  425. var targetTuple = catItemInSeries && _.map(catItemInSeries.items[originalIndex].t, function (tuple) {
  426. return tuple.u;
  427. }) || [];
  428. targetTuple.push(measureMun);
  429. // Search the target tuple in the cross joined tuple set of the multi measure series, and return the index
  430. var resultIndex = -1;
  431. if (multiSeriesResultItem.items.length !== 0) {
  432. _.find(multiSeriesResultItem.items, function (tuple, idx) {
  433. var muns = _.pluck(tuple.t, 'u');
  434. var matched = _.difference(targetTuple, muns).length === 0 && !_.contains(processedIndices, idx);
  435. if (matched) {
  436. resultIndex = idx;
  437. }
  438. return matched;
  439. });
  440. }
  441. return resultIndex;
  442. },
  443. _getMultiMeasureMun: function _getMultiMeasureMun(index) {
  444. this._multiMeasureSlotAPI = this._multiMeasureSlotAPI || _.find(this.mappingAPI.getSlotAPIs(), function (slotAPI) {
  445. return slotAPI.isMultiMeasuresValue();
  446. });
  447. var dataItemAPIs = this._multiMeasureSlotAPI.getDataItemAPIs();
  448. return dataItemAPIs[index].getUniqueId();
  449. },
  450. /**
  451. * Find the slot ID where the multiMeasureSeries is
  452. */
  453. _getMultiMeasureSeriesId: function _getMultiMeasureSeriesId() {
  454. var slotAPIs = this.mappingAPI.getSlotAPIs();
  455. var multiMeasureSeriesSlot = _.find(slotAPIs, function (slotAPI) {
  456. return slotAPI.isMultiMeasuresSeries();
  457. });
  458. return multiMeasureSeriesSlot && multiMeasureSeriesSlot.getId();
  459. }
  460. });
  461. return PostprocessorClass;
  462. });
  463. //# sourceMappingURL=PostProcessMeasuresAsSeries.js.map