@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
217 lines (190 loc) • 8.01 kB
text/typescript
// (C) 2007-2019 GoodData Corporation
import cloneDeep = require("lodash/cloneDeep");
import get = require("lodash/get");
import isEmpty = require("lodash/isEmpty");
import isNumber = require("lodash/isNumber");
import * as invariant from "invariant";
import { AFM, Execution } from "@gooddata/typings";
import { IntlShape } from "react-intl";
import { isSomeHeaderPredicateMatched } from "../../../../helpers/headerPredicate";
import { IHeaderPredicate } from "../../../../interfaces/HeaderPredicate";
import {
IDrillEventIntersectionElementExtended,
IDrillEventExtended,
IDrillEventContextHeadlineExtended,
} from "../../../../interfaces/DrillEvents";
import { VisualizationTypes } from "../../../../constants/visualizationTypes";
import { IHeadlineData, IHeadlineDataItem } from "../../../../interfaces/Headlines";
import { IHeadlineDrillItemContext } from "../types";
export interface IHeadlineExecutionData {
measureHeaderItem: Execution.IMeasureHeaderItem["measureHeaderItem"];
value: Execution.DataValue;
}
function createHeadlineDataItem(executionDataItem: IHeadlineExecutionData): IHeadlineDataItem {
if (!executionDataItem) {
return null;
}
return {
localIdentifier: executionDataItem.measureHeaderItem.localIdentifier,
title: executionDataItem.measureHeaderItem.name,
value: executionDataItem.value ? String(executionDataItem.value) : null,
format: executionDataItem.measureHeaderItem.format,
isDrillable: false,
};
}
function createTertiaryItem(executionData: IHeadlineExecutionData[], intl: IntlShape): IHeadlineDataItem {
const secondaryHeaderItem = get(executionData, [1, "measureHeaderItem"]);
if (!secondaryHeaderItem) {
return null;
}
const primaryValueString = get(executionData, [0, "value"]);
const primaryValue = primaryValueString !== null ? Number(primaryValueString) : null;
const secondaryValueString = get(executionData, [1, "value"]);
const secondaryValue = secondaryValueString !== null ? Number(secondaryValueString) : null;
const tertiaryTitle = intl.formatMessage({ id: "visualizations.headline.tertiary.title" });
const isCountableValue = isNumber(primaryValue) && isNumber(secondaryValue);
const tertiaryValue =
isCountableValue && secondaryValue !== 0
? ((primaryValue - secondaryValue) / secondaryValue) * 100
: null;
return {
localIdentifier: "tertiaryIdentifier",
title: tertiaryTitle,
value: tertiaryValue !== null ? String(tertiaryValue) : null,
format: null,
isDrillable: false,
};
}
function getExecutionResponseMeasureHeaders(
executionResponse: Execution.IExecutionResponse,
): Execution.IMeasureHeaderItem[] {
return get(executionResponse, ["dimensions", 0, "headers", 0, "measureGroupHeader", "items"], []);
}
/**
* Get tuple of measure header items with related data value by index position from executionResponse and
* executionResult.
*
* @param executionResponse
* @param executionResult
* @returns {any[]}
*/
function getExecutionData(
executionResponse: Execution.IExecutionResponse,
executionResult: Execution.IExecutionResult,
): IHeadlineExecutionData[] {
const headerItems = getExecutionResponseMeasureHeaders(executionResponse);
return headerItems.map((item, index) => {
const value = get(executionResult, ["data", index]);
invariant(value !== undefined, "Undefined execution value data for headline transformation");
invariant(item.measureHeaderItem, "Missing expected measureHeaderItem");
return {
measureHeaderItem: item.measureHeaderItem,
value,
};
});
}
/**
* Get {HeadlineData} used by the {Headline} component.
*
* @param executionResponse - The execution response with dimensions definition.
* @param executionResult - The execution result with an actual data values.
* @param intl - Required localization for compare item title
* @returns {*}
*/
export function getHeadlineData(
executionResponse: Execution.IExecutionResponse,
executionResult: Execution.IExecutionResult,
intl: IntlShape,
): IHeadlineData {
const executionData = getExecutionData(executionResponse, executionResult);
const primaryItem = createHeadlineDataItem(executionData[0]);
const secondaryItem = createHeadlineDataItem(executionData[1]);
const secondaryItemProp = secondaryItem ? { secondaryItem } : {};
const tertiaryItem = createTertiaryItem(executionData, intl);
const tertiaryItemProp = tertiaryItem ? { tertiaryItem } : {};
return {
primaryItem,
...secondaryItemProp,
...tertiaryItemProp,
};
}
function findMeasureHeaderItem(
localIdentifier: AFM.Identifier,
executionResponse: Execution.IExecutionResponse,
) {
const measureGroupHeaderItems = getExecutionResponseMeasureHeaders(executionResponse);
return measureGroupHeaderItems
.map(item => item.measureHeaderItem)
.find(header => header !== undefined && header.localIdentifier === localIdentifier);
}
/**
* Take headline data and apply list of drillable items.
* The method will return copied collection of the headline data with altered drillable status.
*
* @param headlineData - The headline data that we want to change the drillable status.
* @param drillableItems - list of drillable items {uri, identifier}
* @param executionRequest - Request with required measure id (uri or identifier) for activation of drill eventing
* @param executionResponse - Response headers for drilling predicate matching
* @returns altered headlineData
*/
export function applyDrillableItems(
headlineData: IHeadlineData,
drillableItems: IHeaderPredicate[],
executionRequest: AFM.IExecution["execution"],
executionResponse: Execution.IExecutionResponse,
): IHeadlineData {
const data = cloneDeep(headlineData);
const { primaryItem, secondaryItem } = data;
const [primaryItemHeader, secondaryItemHeader] = getExecutionResponseMeasureHeaders(executionResponse);
if (!isEmpty(primaryItem) && !isEmpty(primaryItemHeader)) {
primaryItem.isDrillable = isSomeHeaderPredicateMatched(
drillableItems,
primaryItemHeader,
executionRequest.afm,
executionResponse,
);
}
if (!isEmpty(secondaryItem) && !isEmpty(secondaryItemHeader)) {
secondaryItem.isDrillable = isSomeHeaderPredicateMatched(
drillableItems,
secondaryItemHeader,
executionRequest.afm,
executionResponse,
);
}
return data;
}
/**
* Build drill event data (object with execution and drill context) from the data obtained by clicking on the {Headline}
* component an from the execution objects.
*
* @param itemContext - data received from the click on the {Headline} component.
* @param executionRequest - The execution request with AFM and ResultSpec.
* @param executionResponse - The execution response with dimensions definition.
* @returns {*}
*/
export function buildDrillEventData(
itemContext: IHeadlineDrillItemContext,
executionRequest: AFM.IExecution["execution"],
executionResponse: Execution.IExecutionResponse,
): IDrillEventExtended {
const measureHeaderItem = findMeasureHeaderItem(itemContext.localIdentifier, executionResponse);
if (!measureHeaderItem) {
throw new Error("The measure uri has not been found in execution response!");
}
const intersectionElement: IDrillEventIntersectionElementExtended = {
header: {
measureHeaderItem,
},
};
const drillContext: IDrillEventContextHeadlineExtended = {
type: VisualizationTypes.HEADLINE,
element: itemContext.element,
value: itemContext.value,
intersection: [intersectionElement],
};
return {
executionContext: executionRequest.afm,
drillContext,
};
}