'use strict'; /* *+------------------------------------------------------------------------+ *| Licensed Materials - Property of IBM *| IBM Cognos Products: BI Dashboard *| (C) Copyright IBM Corp. 2017, 2019 *| *| US Government Users Restricted Rights - Use, duplication or disclosure *| restricted by GSA ADP Schedule Contract with IBM Corp. *+------------------------------------------------------------------------+ */ define(['jquery', 'underscore', './VisQueryPostProcessor', '../../nls/StringResources', '../QueryResultDataUtils', '../../../../apiHelpers/SlotAPIHelper'], function ($, _, VisQueryPostprocessor, StringResources, QueryResultDataUtils, SlotAPIHelper) { 'use strict'; /** * This Class does Query result response post-processing such as processing the original query result * into a data re-structure to support multiple measures as series. **/ var PostprocessorClass = VisQueryPostprocessor.extend({ /** * @Constructor * @param {Object} options */ init: function init(options) { PostprocessorClass.inherited('init', this, arguments); this.mappingAPI = options.mappingAPI; }, /** * Process Query result based on the passed options to constructor. * For instance, limit the rows of datapoints so that in the response, for * specified dataitem, 'product', there will be only three tuples returned * * @return {QueryResultData} */ _processData: function _processData() { if (this._queryResultData && this._queryResultData.data && this._canProcess()) { var result = this._categorizeDataItems(); // post-process the data items this._processDataItems(result); // post-process the data points this._processDataPoints(result); } //Process Done return this._queryResultData; }, /** * Determine whether multiple measure series is supported */ _canProcess: function _canProcess() { var slotAPIs = this.mappingAPI.getSlotAPIs(); var hasMultiMeasuresSeries = false, hasMultiMeasuresVal = false; _.each(slotAPIs, function (slotAPI) { hasMultiMeasuresSeries = hasMultiMeasuresSeries || slotAPI.isMultiMeasuresSeries(); hasMultiMeasuresVal = hasMultiMeasuresVal || slotAPI.isMultiMeasuresValue(); }); return hasMultiMeasuresSeries && hasMultiMeasuresVal; }, /** * Get the mapped projectionId for each dataItem. * * note: the logic here should be the same in visQueryBuilder.js:_slotToProjections() */ _getQueryProjectionId: function _getQueryProjectionId(slotAPI, dataItemIdx) { // We don't project MultiMeasureSeries dataItem if (slotAPI.isMultiMeasuresSeriesOrValue(dataItemIdx)) return null; var validDataItemIds = _.filter(slotAPI.getDataItemRefs(), function (dataItemUniqueId, ind) { return !slotAPI.isMultiMeasuresSeriesOrValue(ind); }); if (slotAPI.isStacked() && validDataItemIds.length > 1) { return slotAPI.getId(); } return slotAPI.getDataItemAPI(dataItemIdx).getUniqueId(); }, /** * Categorize the existing data items by category and ordinals * Keep track of data items original index and also the slot Id it which the data item is mapped. * The original index later allows the data points (prior to restructure) to access the mapping slot and data item. * * Despite the result data being the original data, the slot mapping already includes the multi-measures series mapping. * The slot mapping for mult-measures series needs to be ignored at this stage in order to match with the original data. */ _categorizeDataItems: function _categorizeDataItems() { var result = { cats: [], ords: [], commonIndices: {}, mappedIndices: {}, extraDataItems: [] }; var resultQueryDataItems = this._queryResultData.dataItems; var allProjectionIds = _.map(resultQueryDataItems, function (dataItem) { return dataItem.itemClass.id; }); var mappedProjectionIds = []; var slotAPIs = this.mappingAPI.getSlotAPIs(); for (var i = 0; i < slotAPIs.length; i++) { var slot = slotAPIs[i]; for (var dataItemIdx = 0; dataItemIdx < slot.getDataItemAPIs().length; dataItemIdx++) { var queryProjectionId = this._getQueryProjectionId(slot, dataItemIdx); if (queryProjectionId) { var idx = allProjectionIds.indexOf(queryProjectionId); var newEntry = { item: idx !== -1 ? resultQueryDataItems[idx] : undefined, slotId: slot.getId(), index: idx !== -1 ? idx : undefined }; if (newEntry.index !== undefined) { result.mappedIndices[newEntry.index] = true; if (slot.getFinalSlotType() === 'ordinal' && !slot.getDefinition().multiMeasure) { result.commonIndices[newEntry.index] = true; } } (slot.getFinalSlotType() === 'category' ? result.cats : result.ords).push(newEntry); mappedProjectionIds.push(queryProjectionId); } } } var extraDataItemIds = _.difference(allProjectionIds, mappedProjectionIds); resultQueryDataItems.forEach(function (dataItem, index) { if (extraDataItemIds.indexOf(dataItem.itemClass.id) !== -1) { result.commonIndices[index] = true; result.extraDataItems.push({ dataItem: dataItem }); } }); return result; }, /** * Process the data items * Consolidate the multiple measures to one measure data item and a extra series to indicate the measure */ _processDataItems: function _processDataItems(dataItems) { var resultData = this._queryResultData; var slotAPIs = this.mappingAPI.getSlotAPIs(); // rebuild the dataItems from scratch! resultData.dataItems.length = 0; // maintain the order based on slots _.each(slotAPIs, function (slotAPI) { var dataItem; var isCategory = slotAPI.getFinalSlotType() === 'category'; if (isCategory) { if (slotAPI.isMultiMeasuresSeries()) { var measures = _.map(dataItems.ords, function (ordinal) { if (ordinal && ordinal.item && ordinal.item.itemClass && ordinal.slotId === 'values') { return ordinal.item.itemClass.h[0]; } return undefined; }); measures = _.compact(measures); // apply the multiple measures as a series dataItem = this._getSeriesDataItemforMultipleMeasures(dataItems.cats, measures, slotAPI); } else { dataItem = this._getDataItem(dataItems.cats, slotAPI); } } else if (slotAPI.isMultiMeasuresValue()) { // apply the value for all measures dataItem = this._getValueDataItemforMultipleMeasures(slotAPI); } else { dataItem = this._getDataItem(dataItems.ords, slotAPI); } if (dataItem) { resultData.dataItems.push(dataItem); } }.bind(this)); dataItems.extraDataItems.forEach(function (extraDataItem) { extraDataItem.index = resultData.dataItems.length; resultData.dataItems.push(extraDataItem.dataItem); }); }, /** * Find the data item mapped to a slotAPI */ _getDataItem: function _getDataItem(items, slotAPI) { var slotItem = _.find(items, function (item) { return item.slotId === slotAPI.getId(); }); return slotItem ? slotItem.item : null; }, /** * Collect the unique values from a category data item */ _getCategoryDataItemValues: function _getCategoryDataItemValues(categoryItem, stackIndex) { if (stackIndex > -1) { var stackValues = {}; // collect the unique values _.each(categoryItem.items, function (item) { stackValues[item.t[stackIndex].u] = item.t[stackIndex]; }); var keys = Object.keys(stackValues); return _.map(keys, function (key) { return stackValues[key]; }); } return []; }, /** * Create a category data item for the multiple measures series */ _getSeriesDataItemforMultipleMeasures: function _getSeriesDataItemforMultipleMeasures(cats, measures, slotAPI) { return { itemClass: this._getSeriesDataItemClass(cats, slotAPI), items: this._getSeriesTupleItems(cats, measures, slotAPI) }; }, /** * Collect all data item Ids from a given slot */ _getSlotDataItemIds: function _getSlotDataItemIds(slotAPI) { return _.map(slotAPI.getDataItemAPIs(), function (dataItemAPI) { return dataItemAPI.getItemId(); }); }, /** * Collect all series data item class */ _getSeriesDataItemClass: function _getSeriesDataItemClass(cats, slotAPI) { var ids = this._getSlotDataItemIds(slotAPI); var stackedItem = this._getDataItem(cats, slotAPI); return { id: slotAPI.getId(), h: _.map(ids, function (id, index) { if (slotAPI.isMultiMeasuresSeriesOrValue(index)) { return { u: SlotAPIHelper.MULTI_MEASURES_SERIES, d: StringResources.get('MeasuresCaption') }; } else { if (stackedItem && stackedItem.itemClass) { return _.find(stackedItem.itemClass.h, function (header) { return header.u === id; }); } return undefined; } }) }; }, /** * Collect all series data item tuple items */ _getSeriesTupleItems: function _getSeriesTupleItems(cats, measures, slotAPI) { var rows = []; var baseRows = []; var catItem = this._getDataItem(cats, slotAPI); var stackIds = catItem ? _.map(catItem.itemClass.h, function (header) { return header.u; }) : []; var ids = this._getSlotDataItemIds(slotAPI); _.each(ids, function (id, index) { var _this = this; var isMultiMeasureSeries = slotAPI.isMultiMeasuresSeriesOrValue(index); var values = isMultiMeasureSeries ? measures : this._getCategoryDataItemValues(catItem, stackIds.indexOf(id)); //clean up rows and baseRows, so that no defect occurs in the loop with more than 2 ids. if (rows.length != 0) { baseRows = rows; } rows = []; if (baseRows.length > 0) { if (values.length > 0) { // Should preserve the drop order in multi measure series slot so iterate from baseRows to values _.each(baseRows, function (row) { // duplicate the values tuple var clone = values.slice(); _.each(clone, function (value, valueIndex) { var newRow = valueIndex === 0 ? row : $.extend(true, {}, row); newRow.t[index] = isMultiMeasureSeries ? { u: _this._getMultiMeasureMun(valueIndex), d: value.d, aggregate: value.aggregate } : value; rows.push(newRow); }); }); } else { // since there are no data to duplicate, simply update the base rows with null data _.each(baseRows, function (row) { row.t[index] = { u: null, d: null }; }); } } else { // prepare the base tuple item rows baseRows.push.apply(baseRows, _.map(values, function (value, index) { return { t: [isMultiMeasureSeries ? { u: value && value.u ? _this._getMultiMeasureMun(index) : 'undefined', d: value && value.d ? value.d : 'undefined', aggregate: value.aggregate } : value] }; })); } }.bind(this)); // finish with the base rows if not expecting a category (i.e. not nested) if (!catItem && rows.length === 0) { rows.push.apply(rows, baseRows.slice()); } return rows; }, /** * Create a measure data item for the multiple measure values */ _getValueDataItemforMultipleMeasures: function _getValueDataItemforMultipleMeasures(slotAPI) { return { itemClass: { id: slotAPI.getId(), h: [{ u: SlotAPIHelper.MULTI_MEASURES_VALUE, d: StringResources.get('ValuesCaption') }] } }; }, /** * Process all data point to restructure the data for multiple measures value and series */ _processDataPoints: function _processDataPoints(dataItems) { // prepare the new index for each slot var newIndices = this._allocateDataPointIndices(dataItems.extraDataItems); // restructured data var newData = []; var resultData = this._queryResultData; var multiMeasureSeriesId = this._getMultiMeasureSeriesId(); _.each(resultData.data, function (datapoint) { this._restructureDataPoint(dataItems, datapoint, newIndices, newData, multiMeasureSeriesId); }.bind(this)); resultData.data = newData; }, /** * Allocate the datapoint index for each data item. * Since the multiple measures are wrapped into a single measure with an extra series for the measures, * None of the slots would have multiple data items. * * ie. category, multimeasure_series, category(series), value * indices: { * seriesIndex: 1, * valueIndex: 3, * catIndices: [0, 2] * } */ _allocateDataPointIndices: function _allocateDataPointIndices(extraDataItems) { var catIndex = 0; var commonValueIndex = 0; /** * [todo] livewidget-cleanup: need to be refactored to use the new api. */ var slotAPIs = this.mappingAPI.getSlotAPIs(); var indices = { seriesIndex: -1, valueIndex: -1, commonIndices: [], catIndices: [] }; _.each(slotAPIs, function (slotAPI, slotIndex) { var isCategory = slotAPI.getFinalSlotType() === 'category'; if (isCategory) { if (slotAPI.isMultiMeasuresSeries()) { indices.seriesIndex = slotIndex; } else { indices.catIndices[catIndex++] = slotIndex; } } else if (slotAPI.isMultiMeasuresValue()) { indices.valueIndex = slotIndex; } else { indices.commonIndices[commonValueIndex++] = slotIndex; } }.bind(this)); extraDataItems.forEach(function (dataItem) { return indices.commonIndices[commonValueIndex++] = dataItem.index; }); return indices; }, /** * Restructure a single data point. * Consolidate multiple measures to one value * Categorize the measures to one category */ _restructureDataPoint: function _restructureDataPoint(dataItems, datapoint, indices, newData, multiMeasureSeriesId) { var _this2 = this; var catValueIndex = 0; var commonValueIndex = 0; var point = []; var ordValues = []; // has to be in order var catItemInSeries = void 0; var seriesIndexValue = 0; var slotAPIs = this.mappingAPI.getSlotAPIs(); // Prepare the common portion of the data point _.each(datapoint.pt, function (value, valueIndex) { var category = _.find(dataItems.cats, function (cat) { return cat.index === valueIndex; }); if (category) { var slotAPI = _.find(slotAPIs, function (slotAPI) { return slotAPI.getId() === category.slotId; }); if (slotAPI.isMultiMeasuresSeries()) { catItemInSeries = category.item; seriesIndexValue = value; } else { point[indices.catIndices[catValueIndex++]] = value; } } else if (dataItems.commonIndices[valueIndex]) { point[indices.commonIndices[commonValueIndex++]] = value; } else if (dataItems.mappedIndices[valueIndex]) { // Keep track of the remaining ordinal values (as long as they're 'valid' - mapped to a slot) ordValues.push(value); } }); // Duplicate the data point by each measure var processedIndices = []; _.each(ordValues, function (ordValue, indexValue) { var pt = indexValue === 0 ? point : point.slice(); pt[indices.seriesIndex] = _this2._getSeriesIndex(dataItems, indexValue, catItemInSeries, seriesIndexValue, processedIndices, multiMeasureSeriesId); pt[indices.valueIndex] = ordValue; newData.push({ pt: pt }); processedIndices.push(indexValue); }); }, _getSeriesIndex: function _getSeriesIndex(dataItems, ordIndex, catItemInSeries, originalIndex, processedIndices, multiMeasureSeriesId) { // The multiSeriesResultItem has tuples built from _getSeriesTupleItems. It's a cross joined tuple set of the multi measure series var multiSeriesResultItem = this._queryResultData && this._queryResultData.dataItems && this._queryResultData.dataItems[QueryResultDataUtils.getDataItemIndex(this._queryResultData, multiMeasureSeriesId)]; // The target tuple to search in multiSeriesResultItem and return its index var measureMun = this._getMultiMeasureMun(ordIndex); var targetTuple = catItemInSeries && _.map(catItemInSeries.items[originalIndex].t, function (tuple) { return tuple.u; }) || []; targetTuple.push(measureMun); // Search the target tuple in the cross joined tuple set of the multi measure series, and return the index var resultIndex = -1; if (multiSeriesResultItem.items.length !== 0) { _.find(multiSeriesResultItem.items, function (tuple, idx) { var muns = _.pluck(tuple.t, 'u'); var matched = _.difference(targetTuple, muns).length === 0 && !_.contains(processedIndices, idx); if (matched) { resultIndex = idx; } return matched; }); } return resultIndex; }, _getMultiMeasureMun: function _getMultiMeasureMun(index) { this._multiMeasureSlotAPI = this._multiMeasureSlotAPI || _.find(this.mappingAPI.getSlotAPIs(), function (slotAPI) { return slotAPI.isMultiMeasuresValue(); }); var dataItemAPIs = this._multiMeasureSlotAPI.getDataItemAPIs(); return dataItemAPIs[index].getUniqueId(); }, /** * Find the slot ID where the multiMeasureSeries is */ _getMultiMeasureSeriesId: function _getMultiMeasureSeriesId() { var slotAPIs = this.mappingAPI.getSlotAPIs(); var multiMeasureSeriesSlot = _.find(slotAPIs, function (slotAPI) { return slotAPI.isMultiMeasuresSeries(); }); return multiMeasureSeriesSlot && multiMeasureSeriesSlot.getId(); } }); return PostprocessorClass; }); //# sourceMappingURL=PostProcessMeasuresAsSeries.js.map