@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
306 lines (252 loc) • 9.56 kB
text/typescript
// (C) 2007-2021 GoodData Corporation
import remove = require("lodash/remove");
import cloneDeep = require("lodash/cloneDeep");
import sortedUniq = require("lodash/sortedUniq");
import clone = require("lodash/clone");
import without = require("lodash/without");
import omit = require("lodash/omit");
import sortBy = require("lodash/sortBy");
import get = require("lodash/get");
import { AFM } from "@gooddata/typings";
import { IntlShape } from "react-intl";
import {
isMappingHeaderAttribute,
isMappingHeaderMeasureItem,
IMappingHeader,
} from "../../../../interfaces/MappingHeader";
import { IIndexedTotalItem, ITotalWithData } from "../../../../interfaces/Totals";
import { IAlignPoint, ITotalsDataSource, ITotalTypeWithTitle } from "../../../../interfaces/Table";
import { getFooterHeight } from "../utils/layout";
export const AVAILABLE_TOTALS: AFM.TotalType[] = ["sum", "max", "min", "avg", "med", "nat"];
export const isNativeTotal = (total: AFM.ITotalItem) => {
return total && total.type === "nat";
};
export const getAttributeDimension = (
attributeIdentifier: string,
resultSpec: AFM.IResultSpec,
): AFM.IDimension => {
return resultSpec.dimensions.find(
dimension => !!dimension.itemIdentifiers.find(attribute => attribute === attributeIdentifier),
);
};
const getNativeTotalAttributeIdentifiers = (total: AFM.ITotalItem, resultSpec: AFM.IResultSpec): string[] => {
const attributeIdentifiers = getAttributeDimension(total.attributeIdentifier, resultSpec).itemIdentifiers;
const totalAttributeIndex = attributeIdentifiers.findIndex(
attributeIdentifier => attributeIdentifier === total.attributeIdentifier,
);
return attributeIdentifiers.slice(0, totalAttributeIndex);
};
export const getNativeTotals = (
totals: AFM.ITotalItem[],
resultSpec: AFM.IResultSpec,
): AFM.INativeTotalItem[] => {
if (!totals) {
return [];
}
const afmNativeTotals: AFM.INativeTotalItem[] = totals
.filter(total => isNativeTotal(total))
.map(nativeTotal => ({
measureIdentifier: nativeTotal.measureIdentifier,
attributeIdentifiers: getNativeTotalAttributeIdentifiers(nativeTotal, resultSpec),
}));
return afmNativeTotals;
};
export const getTotalsFromResultSpec = (resultSpec: AFM.IResultSpec): AFM.ITotalItem[] => {
return resultSpec && resultSpec.dimensions
? resultSpec.dimensions.reduce(
(totals: AFM.ITotalItem[], dimension) =>
dimension && dimension.totals ? totals.concat(dimension.totals) : totals,
[],
)
: [];
};
function getTotalsList(intl: IntlShape): ITotalTypeWithTitle[] {
return AVAILABLE_TOTALS.map(type => ({
type,
title: intl.formatMessage({ id: `visualizations.totals.dropdown.title.${type}` }),
}));
}
export function getTotalsDataSource(usedTotals: ITotalWithData[], intl: IntlShape): ITotalsDataSource {
const usedTotalsTypes: AFM.TotalType[] = usedTotals.map((total: ITotalWithData) => total.type);
const list: ITotalTypeWithTitle[] = getTotalsList(intl).map((total: ITotalTypeWithTitle) => ({
...total,
disabled: usedTotalsTypes.includes(total.type),
}));
list.unshift({
title: "visualizations.totals.dropdown.heading",
role: "header",
});
return {
rowsCount: list.length,
getObjectAt: (index: number) => list[index],
};
}
export function createTotalItem(
type: AFM.TotalType,
outputMeasureIndexes: number[] = [],
values: number[] = [],
): ITotalWithData {
return {
type,
outputMeasureIndexes,
values,
};
}
export function orderTotals(totalsUnordered: IIndexedTotalItem[]): IIndexedTotalItem[] {
return sortBy(totalsUnordered, total => AVAILABLE_TOTALS.indexOf(total.type));
}
export function toggleCellClass(
parentReference: Element,
tableColumnIndex: number,
isHighlighted: boolean,
className: string,
): void {
const cells: NodeListOf<Element> = parentReference.querySelectorAll(`.col-${tableColumnIndex}`);
Array.from(cells).forEach((cell: Element) => {
if (isHighlighted) {
cell.classList.add(className);
} else {
cell.classList.remove(className);
}
});
}
export function resetRowClass(
parentReference: Element,
className: string,
selector: string,
rowIndexToBeSet: number = null,
): void {
const rows: NodeListOf<Element> = parentReference.querySelectorAll(selector);
Array.from(rows).forEach((r: Element) => r.classList.remove(className));
if (rows.length && rowIndexToBeSet !== null) {
const row: Element = rows[rowIndexToBeSet];
row.classList.add(className);
}
}
export function removeTotalsRow(
totals: ITotalWithData[],
totalItemTypeToRemove: AFM.TotalType,
): ITotalWithData[] {
const updatedTotals: ITotalWithData[] = cloneDeep(totals);
remove(updatedTotals, total => total.type === totalItemTypeToRemove);
return updatedTotals;
}
export function isTotalUsed(totals: ITotalWithData[], totalItemType: AFM.TotalType): boolean {
return totals.some(row => row.type === totalItemType);
}
export function addTotalsRow(totals: ITotalWithData[], totalItemTypeToAdd: AFM.TotalType): ITotalWithData[] {
const updatedTotals: ITotalWithData[] = cloneDeep(totals);
if (isTotalUsed(updatedTotals, totalItemTypeToAdd)) {
return updatedTotals;
}
const total: ITotalWithData = createTotalItem(totalItemTypeToAdd);
updatedTotals.push(total);
return updatedTotals;
}
export function updateTotalsRemovePosition(
tableBoundingRect: ClientRect,
totals: ITotalWithData[],
isTotalsEditAllowed: boolean,
totalsAreVisible: boolean,
removeWrapper: HTMLElement,
): void {
if (!isTotalsEditAllowed) {
return;
}
const translateY: number =
tableBoundingRect.height - getFooterHeight(totals, isTotalsEditAllowed, totalsAreVisible);
removeWrapper.style.bottom = "auto";
removeWrapper.style.top = `${translateY}px`;
}
export function getAddTotalDropdownAlignPoints(isLastColumn: boolean = false): IAlignPoint[] {
return isLastColumn
? [
{ align: "tc br", offset: { x: 30, y: -3 } }, // top right
{ align: "bc tr", offset: { x: 30, y: 50 } }, // bottom right
]
: [
{ align: "tc bc", offset: { x: 0, y: -3 } }, // top center
{ align: "bc tc", offset: { x: 0, y: 50 } }, // bottom center
];
}
export function shouldShowAddTotalButton(
header: IMappingHeader,
isFirstColumn: boolean,
addingMoreTotalsEnabled: boolean,
): boolean {
return !isFirstColumn && isMappingHeaderMeasureItem(header) && addingMoreTotalsEnabled;
}
export function getFirstMeasureIndex(headers: IMappingHeader[]): number {
const measureOffset = headers.findIndex(header => isMappingHeaderMeasureItem(header));
return measureOffset === -1 ? 0 : measureOffset;
}
export function hasTableColumnTotalEnabled(
outputMeasureIndexes: number[],
tableColumnIndex: number,
firstMeasureIndex: number,
): boolean {
const index = tableColumnIndex - firstMeasureIndex;
return outputMeasureIndexes && outputMeasureIndexes.includes(index);
}
export function addMeasureIndex(
totals: ITotalWithData[],
headers: IMappingHeader[],
totalType: AFM.TotalType,
tableColumnIndex: number,
): ITotalWithData[] {
const index: number = tableColumnIndex - getFirstMeasureIndex(headers);
return totals.map((total: ITotalWithData) => {
if (total.type !== totalType) {
return total;
}
const outputMeasureIndexes: number[] = clone(total.outputMeasureIndexes);
outputMeasureIndexes.push(index);
outputMeasureIndexes.sort((a, b) => a - b);
return {
...total,
outputMeasureIndexes: sortedUniq(outputMeasureIndexes),
};
});
}
export function removeMeasureIndex(
totals: ITotalWithData[],
headers: IMappingHeader[],
totalType: AFM.TotalType,
tableColumnIndex: number,
): ITotalWithData[] {
const index: number = tableColumnIndex - getFirstMeasureIndex(headers);
return totals.map((total: ITotalWithData) => {
if (total.type !== totalType) {
return total;
}
const outputMeasureIndexes: number[] = without(total.outputMeasureIndexes, index);
return {
...total,
outputMeasureIndexes,
};
});
}
export function getTotalsDefinition(totalsWithValues: ITotalWithData[]): IIndexedTotalItem[] {
const totalsWithoutValues: IIndexedTotalItem[] = totalsWithValues.map(
(total: IIndexedTotalItem) => omit(total, "values") as IIndexedTotalItem,
);
return orderTotals(totalsWithoutValues);
}
export function shouldShowTotals(headers: IMappingHeader[]): boolean {
if (headers.length < 1) {
return false;
}
const onlyMeasures: boolean = headers.every((header: IMappingHeader) =>
isMappingHeaderMeasureItem(header),
);
const onlyAttributes: boolean = headers.every((header: IMappingHeader) =>
isMappingHeaderAttribute(header),
);
return !(onlyAttributes || onlyMeasures);
}
export const getColumnTotalsFromResultSpec = (source: AFM.IResultSpec) => {
return get(source, "dimensions[0].totals", []);
};
export default {
getColumnTotalsFromResultSpec,
};