UNPKG

@gooddata/react-components

Version:

GoodData.UI - A powerful JavaScript library for building analytical applications

895 lines (757 loc) • 32.6 kB
// (C) 2019-2020 GoodData Corporation import set = require("lodash/set"); import get = require("lodash/get"); import uniq = require("lodash/uniq"); import uniqBy = require("lodash/uniqBy"); import negate = require("lodash/negate"); import includes = require("lodash/includes"); import every = require("lodash/every"); import forEach = require("lodash/forEach"); import cloneDeep = require("lodash/cloneDeep"); import isEmpty = require("lodash/isEmpty"); import flatMap = require("lodash/flatMap"); import compact = require("lodash/compact"); import without = require("lodash/without"); import { IntlShape } from "react-intl"; import { VisType, VisualizationTypes } from "../../constants/visualizationTypes"; import * as BucketNames from "../../constants/bucketNames"; import { OverTimeComparisonType, OverTimeComparisonTypes } from "../../interfaces/OverTimeComparison"; import { Execution, VisualizationObject } from "@gooddata/typings"; import { IFiltersBucketItem, IBucketItem, IBucket, IExtendedReferencePoint, IUiConfig, IBucketsUiConfig, IBucketUiConfig, IFilters, IBucketFilter, IDateFilter, isDateFilter, isAttributeFilter, isMeasureValueFilter, } from "../interfaces/Visualization"; import { DATE_DATASET_ATTRIBUTE, ATTRIBUTE, DATE, METRIC, BUCKETS, SHOW_ON_SECONDARY_AXIS, } from "../constants/bucket"; import { UICONFIG } from "../constants/uiConfig"; import { getTranslation } from "./translations"; import { filterOutEmptyBuckets } from "../../helpers/mdObjBucketHelper"; import { SUPPORTED_MEASURE_BUCKETS } from "../../components/visualizations/chart/chartOptions/bulletChartOptions"; export function sanitizeFilters(newReferencePoint: IExtendedReferencePoint): IExtendedReferencePoint { const attributeBucketItems = getAllAttributeItems(newReferencePoint.buckets); const measureBucketItems = getAllMeasureItems(newReferencePoint.buckets); newReferencePoint.filters = newReferencePoint.filters || { localIdentifier: "filters", items: [], }; const filteredFilters = newReferencePoint.filters.items.filter((filterBucketItem: IFiltersBucketItem) => { const filter = filterBucketItem.filters[0]; if (isAttributeFilter(filter) || isDateFilter(filter)) { if (filterBucketItem.autoCreated === false) { return true; } return attributeBucketItems.some( (attributeBucketItem: IBucketItem) => attributeBucketItem.attribute === filter.attribute, ); } else if (isMeasureValueFilter(filter)) { if (attributeBucketItems.length === 0) { return false; } return measureBucketItems.some( (measureBucketItem: IBucketItem) => measureBucketItem.localIdentifier === filter.measureLocalIdentifier, ); } return false; }); return { ...newReferencePoint, filters: { ...newReferencePoint.filters, items: filteredFilters, }, }; } export function isDerivedBucketItem(measureItem: IBucketItem): boolean { return !!measureItem.masterLocalIdentifier; } function isArithmeticBucketItem(bucketItem: IBucketItem): boolean { return !!bucketItem.operandLocalIdentifiers; } function isDerivedOfTypeBucketItem(measureItem: IBucketItem, derivedType: OverTimeComparisonType): boolean { if (!isDerivedBucketItem(measureItem)) { return false; } return measureItem.overTimeComparisonType === derivedType; } function findDerivedTypesReferencedByArithmeticMeasure( measure: IBucketItem, allMeasures: IBucketItem[], visitedMeasures: Set<string>, ): Set<OverTimeComparisonType> { return measure.operandLocalIdentifiers.reduce( (types: Set<OverTimeComparisonType>, operandIdentifier: string) => { if (operandIdentifier === null || visitedMeasures.has(operandIdentifier)) { return types; } const operand: IBucketItem = findMeasureByLocalIdentifier(operandIdentifier, allMeasures); if (operand === undefined) { return types; } if (isArithmeticBucketItem(operand)) { visitedMeasures.add(operandIdentifier); findDerivedTypesReferencedByArithmeticMeasure(operand, allMeasures, visitedMeasures).forEach( (type: OverTimeComparisonType) => types.add(type), ); } else if (isDerivedBucketItem(operand) && !types.has(operand.overTimeComparisonType)) { types.add(operand.overTimeComparisonType); } return types; }, new Set(), ); } /** * Get array of unique over time comparison types used in ancestors of the provided arithmetic measure. * * @param measure - the (possibly) arithmetic measure * @param buckets - all buckets * @return empty array if there are no derived measures in the arithmetic measure ancestors, empty array if provided * measure is not arithmetic, array of unique {OverTimeComparisonType} of derived ancestor measures found in arithmetic * measure tree. */ export function getDerivedTypesFromArithmeticMeasure( measure: IBucketItem, buckets: IBucket[], ): OverTimeComparisonType[] { if (!isArithmeticBucketItem(measure)) { return []; } const allMeasures = flatMap<IBucket, IBucketItem>(buckets, bucket => bucket.items); const overTimeComparisonTypes = findDerivedTypesReferencedByArithmeticMeasure( measure, allMeasures, new Set(), ); return Array.from(overTimeComparisonTypes); } export function filterOutDerivedMeasures(measures: IBucketItem[]): IBucketItem[] { return measures.filter(measure => !isDerivedBucketItem(measure)); } function isArithmeticMeasureFromDerived(measure: IBucketItem, buckets: IBucket[]): boolean { return getDerivedTypesFromArithmeticMeasure(measure, buckets).length > 0; } export function filterOutArithmeticMeasuresFromDerived( measures: IBucketItem[], buckets: IBucket[], ): IBucketItem[] { return measures.filter(measure => !isArithmeticMeasureFromDerived(measure, buckets)); } function isArithmeticMeasureFromDerivedOfTypeOnly( measure: IBucketItem, buckets: IBucket[], derivedType: OverTimeComparisonType, ): boolean { const arithmeticMeasureDerivedTypes = getDerivedTypesFromArithmeticMeasure(measure, buckets); return arithmeticMeasureDerivedTypes.length === 1 && arithmeticMeasureDerivedTypes[0] === derivedType; } export function keepOnlyMasterAndDerivedMeasuresOfType( measures: IBucketItem[], derivedType: OverTimeComparisonType, ): IBucketItem[] { return measures.filter( measure => !isDerivedBucketItem(measure) || isDerivedOfTypeBucketItem(measure, derivedType), ); } export function filterOutIncompatibleArithmeticMeasures( measures: IBucketItem[], buckets: IBucket[], derivedOfTypeToKeep: OverTimeComparisonType, ): IBucketItem[] { return measures.filter( (measure: IBucketItem) => !isArithmeticBucketItem(measure) || !isArithmeticMeasureFromDerived(measure, buckets) || isArithmeticMeasureFromDerivedOfTypeOnly(measure, buckets, derivedOfTypeToKeep), ); } export function isDateBucketItem(bucketItem: IBucketItem): boolean { return !!bucketItem && bucketItem.attribute === DATE_DATASET_ATTRIBUTE; } export const isNotDateBucketItem = negate(isDateBucketItem); export function getDateFilter(filtersBucket: IFilters): IDateFilter { const dateFiltersInclEmpty = flatMap(filtersBucket.items, filterItem => { const filters = get<IFiltersBucketItem, "filters", IBucketFilter[]>(filterItem, "filters", []); return filters.find(isDateFilter); }); const dateFilters = compact(dateFiltersInclEmpty); return dateFilters.length ? dateFilters[0] : null; } export function getComparisonTypeFromFilters(filtersBucket: IFilters): OverTimeComparisonType { if (isEmpty(filtersBucket)) { return OverTimeComparisonTypes.NOTHING; } const dateFilter = getDateFilter(filtersBucket); return !isEmpty(dateFilter) && dateFilter.overTimeComparisonType ? dateFilter.overTimeComparisonType : OverTimeComparisonTypes.NOTHING; } function bucketSupportsSubtitle(visualizationType: string, bucketLocalIdentifier: string) { if (visualizationType === VisualizationTypes.HEADLINE) { return true; } if (visualizationType === VisualizationTypes.SCATTER) { return bucketLocalIdentifier !== BucketNames.ATTRIBUTE; } if (visualizationType === VisualizationTypes.BUBBLE) { return bucketLocalIdentifier !== BucketNames.VIEW; } if (visualizationType === VisualizationTypes.COMBO) { return bucketLocalIdentifier !== BucketNames.VIEW; } if (visualizationType === VisualizationTypes.PUSHPIN) { return ( bucketLocalIdentifier !== BucketNames.LOCATION && bucketLocalIdentifier !== BucketNames.SEGMENT ); } if (visualizationType === VisualizationTypes.BULLET) { return bucketLocalIdentifier !== BucketNames.VIEW; } return false; } export function setBucketTitles( referencePoint: IExtendedReferencePoint, visualizationType: string, intl?: IntlShape, ): IUiConfig { const buckets: IBucket[] = get(referencePoint, BUCKETS); const updatedUiConfig: IUiConfig = cloneDeep(get(referencePoint, UICONFIG)); forEach(buckets, (bucket: IBucket) => { const localIdentifier: string = get(bucket, "localIdentifier", ""); // skip disabled buckets if (!get(updatedUiConfig, [BUCKETS, localIdentifier, "enabled"], false)) { return; } if (bucketSupportsSubtitle(visualizationType, localIdentifier)) { const subtitleId = generateBucketSubtitleId(localIdentifier, visualizationType); const subtitle = getTranslation(subtitleId, intl); set(updatedUiConfig, [BUCKETS, localIdentifier, "subtitle"], subtitle); } const titleId = generateBucketTitleId(localIdentifier, visualizationType); const title = getTranslation(titleId, intl); set(updatedUiConfig, [BUCKETS, localIdentifier, "title"], title); }); return updatedUiConfig; } export function generateBucketTitleId(localIdentifier: string, visualizationType: string): string { return `dashboard.bucket.${localIdentifier}_title.${visualizationType}`; } export function generateBucketSubtitleId(localIdentifier: string, visualizationType: string): string { return `dashboard.bucket.${localIdentifier}_subtitle.${visualizationType}`; } export function getItemsCount(buckets: IBucket[], localIdentifier: string): number { return getBucketItems(buckets, localIdentifier).length; } export function getBucketItems(buckets: IBucket[], localIdentifier: string): IBucketItem[] { return get(buckets.find(bucket => bucket.localIdentifier === localIdentifier), "items", []); } // return bucket items matching localIdentifiers from any bucket export function getItemsFromBuckets( buckets: IBucket[], localIdentifiers: string[], types?: string[], ): IBucketItem[] { return localIdentifiers.reduce( (bucketItems, localIdentifier) => bucketItems.concat( types ? getBucketItemsByType(buckets, localIdentifier, types) : getBucketItems(buckets, localIdentifier), ), [], ); } export function getBucketItemsByType( buckets: IBucket[], localIdentifier: string, types: string[], ): IBucketItem[] { const itemsOfType: IBucketItem[] = []; const bucketItems: IBucketItem[] = getBucketItems(buckets, localIdentifier); bucketItems.forEach((item: IBucketItem) => { if (includes(types, item.type)) { itemsOfType.push(item); } }); return itemsOfType; } export function getPreferredBucketItems( buckets: IBucket[], preference: string[], type: string[], ): IBucketItem[] { const bucket = getPreferredBucket(buckets, preference, type); return get(bucket, "items", []); } export function getPreferredBucket(buckets: IBucket[], preference: string[], type: string[]): IBucket { return preference.reduce((result: IBucket, preference: string) => { if (result) { return result; } return buckets.find((bucket: IBucket) => { const preferenceMatch = bucket.localIdentifier === preference; const typeMatch = every(get(bucket, "items", []), item => type.indexOf(item.type) !== -1); return preferenceMatch && typeMatch; }); }, undefined); } export function getAllBucketItemsByType(bucket: IBucket, types: string[]): IBucketItem[] { return bucket.items.reduce((resultItems: IBucketItem[], item: IBucketItem): IBucketItem[] => { if (includes(types, item.type)) { resultItems.push(item); } return resultItems; }, []); } export function getAllItemsByType(buckets: IBucket[], types: string[]): IBucketItem[] { return buckets.reduce( (items: IBucketItem[], bucket: IBucket) => [...items, ...getAllBucketItemsByType(bucket, types)], [], ); } export function removeDuplicateBucketItems(buckets: IBucket[]): IBucket[] { const usedIdentifiersMap: { [key: string]: boolean } = {}; return buckets.map(bucket => { const filteredBucketItems = bucket.items.filter(bucketItem => { const isDuplicate = usedIdentifiersMap[bucketItem.localIdentifier]; usedIdentifiersMap[bucketItem.localIdentifier] = true; return !isDuplicate; }); return filteredBucketItems.length === bucket.items.length ? bucket : { ...bucket, items: filteredBucketItems, }; }); } export function getTotalsFromBucket( buckets: IBucket[], bucketName: string, ): VisualizationObject.IVisualizationTotal[] { const selectedBucket = buckets.find(bucket => bucket.localIdentifier === bucketName); return get(selectedBucket, "totals", []); } export function getUniqueAttributes(buckets: IBucket[]) { const attributes = getAllItemsByType(buckets, [ATTRIBUTE, DATE]); return uniqBy(attributes, attribute => get(attribute, "attribute")); } export function getMeasures(buckets: IBucket[]) { return getAllItemsByType(buckets, [METRIC]); } export function getFirstValidMeasure(buckets: IBucket[]): IBucketItem { const measures = getMeasures(buckets); const validMeasures = measures.filter(isValidMeasure); return validMeasures[0] || null; } function isValidMeasure(measure: IBucketItem): boolean { if (isArithmeticBucketItem(measure)) { return measure.operandLocalIdentifiers.every( operandLocalIdentifier => operandLocalIdentifier !== null, ); } return true; } export function getFirstAttribute(buckets: IBucket[]): IBucketItem { return getUniqueAttributes(buckets)[0] || null; } export function getMeasureItems(buckets: IBucket[]): IBucketItem[] { const preference = [BucketNames.MEASURES, BucketNames.SECONDARY_MEASURES, BucketNames.TERTIARY_MEASURES]; const preferredMeasures = preference.reduce((acc, pref) => { const prefBucketItems = getPreferredBucketItems(buckets, [pref], [METRIC]); return [...acc, ...prefBucketItems]; }, []); // if not found in prefered bucket use all available measure items if (isEmpty(get(preferredMeasures, "items", []))) { return getMeasures(buckets); } return get(preferredMeasures, "items", []); } export function getBucketItemsWithExcludeByType( buckets: IBucket[], excludedBucket: string[], type: string[], ) { const includedBuckets = buckets.filter( (bucket: IBucket) => !includes(excludedBucket, bucket.localIdentifier), ); return getAllItemsByType(includedBuckets, type); } export function getStackItems(buckets: IBucket[], itemTypes: string[] = [ATTRIBUTE]): IBucketItem[] { const preferredStacks = getPreferredBucket(buckets, [BucketNames.STACK, BucketNames.SEGMENT], itemTypes); return get(preferredStacks, "items", []); } export function getAttributeItems(buckets: IBucket[]): IBucketItem[] { return getAllAttributeItemsWithPreference(buckets, [ BucketNames.LOCATION, BucketNames.VIEW, BucketNames.TREND, ]); } export function getAttributeItemsWithoutStacks(buckets: IBucket[]): IBucketItem[] { return getAttributeItems(buckets).filter(attribute => { return !includes(getStackItems(buckets), attribute); }); } export function getAllCategoriesAttributeItems(buckets: IBucket[]): IBucketItem[] { const stackItemsWithDate = getStackItems(buckets, [ATTRIBUTE, DATE]); return getAttributeItems(buckets).filter((attribute: IBucketItem) => { return !includes(stackItemsWithDate, attribute); }); } export function getAllAttributeItems(buckets: IBucket[]): IBucketItem[] { return getAllItemsByType(buckets, [ATTRIBUTE, DATE]); } function getAllMeasureItems(buckets: IBucket[]): IBucketItem[] { return getAllItemsByType(buckets, [METRIC]); } // get all attributes from buckets, but items from prefered buckets are first export function getAllAttributeItemsWithPreference(buckets: IBucket[], preference: string[]): IBucketItem[] { const preferredAttributes = preference.reduce((acc, pref) => { const prefBucket = getPreferredBucket(buckets, [pref], [ATTRIBUTE, DATE]); return [...acc, ...get(prefBucket, "items", [])]; }, []); const allBucketNames: string[] = buckets.map(bucket => get(bucket, "localIdentifier")); const otherBucketNames: string[] = allBucketNames.filter(bucketName => !includes(preference, bucketName)); const allOtherAttributes = otherBucketNames.reduce( (attributes, bucketName) => attributes.concat(getBucketItemsByType(buckets, bucketName, [ATTRIBUTE, DATE])), [], ); return [...preferredAttributes, ...allOtherAttributes]; } export function getDateItems(buckets: IBucket[]): IBucketItem[] { return getAttributeItemsWithoutStacks(buckets).filter(isDateBucketItem); } function hasItemsAboveLimit(bucket: IBucket, itemsLimit: number): boolean { const masterBucketItems = filterOutDerivedMeasures(bucket.items); return masterBucketItems.length > itemsLimit; } function applyItemsLimit(bucket: IBucket, itemsLimit: number): IBucket { if (itemsLimit !== undefined && hasItemsAboveLimit(bucket, itemsLimit)) { const newBucket = cloneDeep(bucket); newBucket.items = newBucket.items.slice(0, itemsLimit); return newBucket; } return bucket; } function applyUiConfigOnBucket(bucket: IBucket, bucketUiConfig: IBucketUiConfig): IBucket { return applyItemsLimit(bucket, get(bucketUiConfig, "itemsLimit")); } export function applyUiConfig(referencePoint: IExtendedReferencePoint): IExtendedReferencePoint { const buckets: IBucket[] = referencePoint.buckets; const uiConfig: IBucketsUiConfig = referencePoint.uiConfig.buckets; const newBuckets: IBucket[] = buckets.map((bucket: IBucket) => applyUiConfigOnBucket(bucket, uiConfig[bucket.localIdentifier]), ); set(referencePoint, "buckets", newBuckets); return referencePoint; } export function hasBucket(buckets: IBucket[], localIdentifier: string): boolean { return buckets.some(bucket => bucket.localIdentifier === localIdentifier); } export function findBucket(buckets: IBucket[], localIdentifier: string): IBucket { return buckets.find((bucket: IBucket) => get(bucket, "localIdentifier") === localIdentifier); } export function getBucketsByNames(buckets: IBucket[], names: string[]): IBucket[] { return buckets.filter((bucket: IBucket) => includes(names, get(bucket, "localIdentifier"))); } export function getFirstMasterWithDerived(measureItems: IBucketItem[]): IBucketItem[] { const masters = filterOutDerivedMeasures(measureItems); const chosenMaster = masters[0]; return measureItems.filter( measureItem => measureItem.masterLocalIdentifier === chosenMaster.localIdentifier || measureItem === chosenMaster, ); } export function removeAllArithmeticMeasuresFromDerived( extendedReferencePoint: IExtendedReferencePoint, ): IExtendedReferencePoint { const originalBuckets = cloneDeep(extendedReferencePoint.buckets); forEach(extendedReferencePoint.buckets, bucket => { bucket.items = filterOutArithmeticMeasuresFromDerived(bucket.items, originalBuckets); }); return extendedReferencePoint; } export function removeAllDerivedMeasures( extendedReferencePoint: IExtendedReferencePoint, ): IExtendedReferencePoint { forEach(extendedReferencePoint.buckets, bucket => { bucket.items = filterOutDerivedMeasures(bucket.items); }); return extendedReferencePoint; } export function findMasterBucketItem( derivedBucketItem: IBucketItem, bucketItems: IBucketItem[], ): IBucketItem { return bucketItems.find(item => item.localIdentifier === derivedBucketItem.masterLocalIdentifier); } export function findMasterBucketItems(bucketItems: IBucketItem[]): IBucketItem[] { return bucketItems.filter(measure => !isDerivedBucketItem(measure)); } export function findDerivedBucketItems( masterBucketItem: IBucketItem, bucketItems: IBucketItem[], ): IBucketItem[] { return bucketItems.filter(measure => measure.masterLocalIdentifier === masterBucketItem.localIdentifier); } export function findDerivedBucketItem( masterBucketItem: IBucketItem, bucketItems: IBucketItem[], ): IBucketItem { return bucketItems.find( bucketItem => bucketItem.masterLocalIdentifier === masterBucketItem.localIdentifier, ); } export function hasDerivedBucketItems(masterBucketItem: IBucketItem, buckets: IBucket[]): boolean { return buckets.some(bucket => bucket.items.some( bucketItem => bucketItem.masterLocalIdentifier === masterBucketItem.localIdentifier, ), ); } export function getFilteredMeasuresForStackedCharts(buckets: IBucket[]) { const hasStacks = getStackItems(buckets).length > 0; if (hasStacks) { const limitedBuckets = limitNumberOfMeasuresInBuckets(buckets, 1); return getMeasureItems(limitedBuckets); } return getMeasureItems(buckets); } export function noRowsAndHasOneMeasure(buckets: VisualizationObject.IBucket[]): boolean { const measureBucket = buckets.find(bucket => bucket.localIdentifier === BucketNames.MEASURES); const rows = buckets.find(bucket => bucket.localIdentifier === BucketNames.VIEW); const hasOneMeasure = measureBucket && measureBucket.items.length === 1; const hasRows = rows && rows.items.length > 0; return Boolean(hasOneMeasure && !hasRows); } export function noColumnsAndHasOneMeasure(buckets: VisualizationObject.IBucket[]): boolean { const measureBucket = buckets.find(bucket => bucket.localIdentifier === BucketNames.MEASURES); const columns = buckets.find(bucket => bucket.localIdentifier === BucketNames.STACK); const hasOneMeasure = measureBucket && measureBucket.items.length === 1; const hasColumn = columns && columns.items.length > 0; return Boolean(hasOneMeasure && !hasColumn); } export function limitNumberOfMeasuresInBuckets( buckets: IBucket[], measuresLimitCount: number, tryToSelectDerivedWithMaster: boolean = false, ): IBucket[] { const allMeasures = getMeasureItems(buckets); let selectedMeasuresLocalIdentifiers: string[] = []; // try to select measures one per bucket buckets.forEach((bucket: IBucket) => { const currentBucketMeasures: IBucketItem[] = getAllBucketItemsByType(bucket, [METRIC]); if (currentBucketMeasures.length === 0) { return; } selectedMeasuresLocalIdentifiers = getLimitedMeasuresLocalIdentifiers( currentBucketMeasures, 1, allMeasures, measuresLimitCount, tryToSelectDerivedWithMaster, selectedMeasuresLocalIdentifiers, ); }); // if it was not possible to select all measures one per bucket then limit them globally if (selectedMeasuresLocalIdentifiers.length < measuresLimitCount) { selectedMeasuresLocalIdentifiers = getLimitedMeasuresLocalIdentifiers( allMeasures, measuresLimitCount, allMeasures, measuresLimitCount, tryToSelectDerivedWithMaster, selectedMeasuresLocalIdentifiers, ); } return pruneBucketMeasureItems(buckets, selectedMeasuresLocalIdentifiers); } function getLimitedMeasuresLocalIdentifiers( measures: IBucketItem[], measuresLimitCount: number, allMeasures: IBucketItem[], allMeasuresLimitCount: number, tryToSelectDerivedWithMaster: boolean, alreadySelectedMeasures: string[], ): string[] { let selectedMeasures: string[] = alreadySelectedMeasures; // try to select measures one by one together with their dependencies measures.forEach((measure: IBucketItem) => { if (selectedMeasures.length - alreadySelectedMeasures.length === measuresLimitCount) { return; } const measureDependencies = getDependenciesLocalIdentifiers(measure, allMeasures); const measureWithDependencies = [measure.localIdentifier, ...measureDependencies]; if (tryToSelectDerivedWithMaster) { const derivedMeasures = getDerivedLocalIdentifiers(measure, allMeasures); const masterDerivedAndDependencies = [...measureWithDependencies, ...derivedMeasures]; selectedMeasures = tryToSelectMeasures( masterDerivedAndDependencies, selectedMeasures, allMeasuresLimitCount, ); } selectedMeasures = tryToSelectMeasures( measureWithDependencies, selectedMeasures, allMeasuresLimitCount, ); }); return selectedMeasures; } function getDerivedLocalIdentifiers(measure: IBucketItem, allMeasures: IBucketItem[]): string[] { const derivedMeasures = findDerivedBucketItems(measure, allMeasures); return derivedMeasures.map((derivedMeasure: IBucketItem) => derivedMeasure.localIdentifier); } function findMeasureByLocalIdentifier( localIdentifier: string, measures: IBucketItem[], ): IBucketItem | undefined { return measures.find((measure: IBucketItem) => measure.localIdentifier === localIdentifier); } function getDependenciesLocalIdentifiers(measure: IBucketItem, allMeasures: IBucketItem[]): string[] { const directDependencies: string[] = []; if (measure.masterLocalIdentifier) { directDependencies.push(measure.masterLocalIdentifier); } if (measure.operandLocalIdentifiers) { measure.operandLocalIdentifiers .filter(operandLocalIdentifier => operandLocalIdentifier !== null) .forEach((operandLocalIdentifier: string) => { const operandMeasure = findMeasureByLocalIdentifier(operandLocalIdentifier, allMeasures); if (operandMeasure !== undefined) { directDependencies.push(operandLocalIdentifier); } }); } const indirectDependencies: string[] = []; directDependencies.forEach((dependencyLocalIdentifier: string) => { const dependencyMeasure = findMeasureByLocalIdentifier(dependencyLocalIdentifier, allMeasures); const dependenciesOfDependency = getDependenciesLocalIdentifiers(dependencyMeasure, allMeasures); indirectDependencies.push(...dependenciesOfDependency); }); return uniq([...directDependencies, ...indirectDependencies]); } function tryToSelectMeasures(measures: string[], alreadySelectedMeasures: string[], limit: number): string[] { const measuresToBePlaced = without(measures, ...alreadySelectedMeasures); if (measuresToBePlaced.length <= limit - alreadySelectedMeasures.length) { return [...alreadySelectedMeasures, ...measuresToBePlaced]; } return alreadySelectedMeasures; } function pruneBucketMeasureItems(buckets: IBucket[], measureLocalIdentifiersToBeKept: string[]): IBucket[] { return buckets.map( (bucket: IBucket): IBucket => { const prunedItems = bucket.items.filter( (item: IBucketItem) => measureLocalIdentifiersToBeKept.indexOf(item.localIdentifier) > -1 || item.type !== METRIC, ); return { ...bucket, items: prunedItems, }; }, ); } export function isShowOnSecondaryAxis(item: IBucketItem): boolean { return get(item, SHOW_ON_SECONDARY_AXIS, false); } export function setMeasuresShowOnSecondaryAxis(items: IBucketItem[], value: boolean): IBucketItem[] { return items.map((item: IBucketItem) => ({ ...item, [SHOW_ON_SECONDARY_AXIS]: value, })); } export function removeShowOnSecondaryAxis(items: IBucketItem[]): IBucketItem[] { return setMeasuresShowOnSecondaryAxis(items, null); } export function getAllMeasuresShowOnSecondaryAxis(buckets: IBucket[]): IBucketItem[] { return getAllItemsByType(buckets, [METRIC]).filter(isShowOnSecondaryAxis); } export function getItemsLocalIdentifiers(items: IBucketItem[]): string[] { return items.map((item: IBucketItem) => get(item, "localIdentifier", "")); } const getAvailableMeasureBucketsLocalIdentifiers = (type: VisType) => (type === VisualizationTypes.BULLET && SUPPORTED_MEASURE_BUCKETS) || []; export const getOccupiedMeasureBucketsLocalIdentifiers = ( type: VisType, mdObject: VisualizationObject.IVisualizationObjectContent, executionResultData: Execution.DataValue[][], ): VisualizationObject.Identifier[] => { const availableMeasureBucketsLocalIdentifiers = getAvailableMeasureBucketsLocalIdentifiers(type); const buckets: VisualizationObject.IBucket[] = get(mdObject, "buckets", []); const notEmptyMeasureBucketsLocalIdentifiers = filterOutEmptyBuckets(buckets) .map(bucket => bucket.localIdentifier) .filter( (bucketLocalIdentifier: string) => availableMeasureBucketsLocalIdentifiers.indexOf(bucketLocalIdentifier) >= 0, ); return !isEmpty(notEmptyMeasureBucketsLocalIdentifiers) ? notEmptyMeasureBucketsLocalIdentifiers : availableMeasureBucketsLocalIdentifiers.slice(0, executionResultData.length); }; export interface IMeasureBucketItemsLimit { localIdentifier: string; itemsLimit: number; } export const transformMeasureBuckets = ( measureBucketItemsLimits: IMeasureBucketItemsLimit[], buckets: IBucket[], ) => { let unusedMeasures: IBucketItem[] = []; const newBuckets: IBucket[] = measureBucketItemsLimits.map(({ localIdentifier, itemsLimit }) => { const preferedBucketlocalIdentifiers: string[] = localIdentifier === BucketNames.MEASURES ? [BucketNames.MEASURES, BucketNames.SIZE] : localIdentifier === BucketNames.SECONDARY_MEASURES ? [BucketNames.SECONDARY_MEASURES, BucketNames.COLOR] : [localIdentifier]; const preferredBucketItems = getPreferredBucketItems(buckets, preferedBucketlocalIdentifiers, [ METRIC, ]); const measuresToBePlaced = preferredBucketItems.splice(0, itemsLimit); if (measuresToBePlaced.length === 0) { return { localIdentifier, items: unusedMeasures.splice(0, itemsLimit), }; } unusedMeasures = [...unusedMeasures, ...preferredBucketItems]; return { localIdentifier, items: measuresToBePlaced, }; }); return newBuckets.map((bucket: IBucket, bucketIndex: number) => { const bucketItemsLimit = measureBucketItemsLimits[bucketIndex].itemsLimit; const freeSlotsCount = bucketItemsLimit - bucket.items.length; if (freeSlotsCount === 0) { return bucket; } return { ...bucket, items: [...bucket.items, ...unusedMeasures.splice(0, freeSlotsCount)], }; }); };