UNPKG

@gooddata/react-components

Version:

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

374 lines (322 loc) • 11.4 kB
// (C) 2007-2020 GoodData Corporation import partial = require("lodash/partial"); import merge = require("lodash/merge"); import includes = require("lodash/includes"); import isNil = require("lodash/isNil"); import set = require("lodash/set"); import get = require("lodash/get"); import { IAxis, IChartConfig, ISeriesItem, IDataLabelsVisible, IStackMeasuresConfig, IHighChartAxis, IYAxisConfig, IChartOptions, } from "../../../../interfaces/Config"; import { supportedStackingAttributesChartTypes } from "../chartOptionsBuilder"; import { formatAsPercent, getLabelStyle, getLabelsVisibilityConfig } from "./dataLabelsHelpers"; import { getPrimaryChartType, isColumnChart, isComboChart, isLineChart, isPrimaryYAxis, isInvertedChartType, } from "../../utils/common"; import { IDrillConfig } from "../../../../interfaces/DrillEvents"; import { canComboChartBeStackedInPercent } from "../chartOptions/comboChartOptions"; export const NORMAL_STACK = "normal"; export const PERCENT_STACK = "percent"; /** * Set 'normal' stacking config to single series which will overwrite config in 'plotOptions.series' * @param stackMeasures * @param seriesItem */ function handleStackMeasure(stackMeasures: boolean, seriesItem: ISeriesItem): ISeriesItem { return stackMeasures ? { ...seriesItem, stacking: NORMAL_STACK, stack: seriesItem.yAxis, } : seriesItem; } /** * Set 'percent' stacking config to single series which will overwrite config in 'plotOptions.series' * @param stackMeasuresToPercent * @param seriesItem */ function handleStackMeasuresToPercent(stackMeasuresToPercent: boolean, seriesItem: ISeriesItem): ISeriesItem { return stackMeasuresToPercent ? { ...seriesItem, stacking: PERCENT_STACK, stack: seriesItem.yAxis, } : seriesItem; } function getStackingValue(chartOptions: IChartOptions, seriesItem: ISeriesItem): string { const { yAxes, type } = chartOptions; const { stacking, yAxis } = seriesItem; const seriesChartType = seriesItem.type || type; const defaultStackingValue = isComboChart(type) ? null : NORMAL_STACK; return isPrimaryYAxis(yAxes[yAxis]) && !isLineChart(seriesChartType) ? stacking : defaultStackingValue; } function handleDualAxis(chartOptions: IChartOptions, seriesItem: ISeriesItem): ISeriesItem { const { yAxes, type } = chartOptions; const isDualAxis = yAxes.length === 2; if (!isDualAxis && !isComboChart(type)) { return seriesItem; } const { stacking } = seriesItem; // highcharts stack config // percent stack is only applied to primary Y axis const hcStackingConfig = stacking ? { stacking: getStackingValue(chartOptions, seriesItem) } : {}; return { ...seriesItem, ...hcStackingConfig, }; } function handleLabelStyle(chartOptions: IChartOptions, seriesItem: ISeriesItem): ISeriesItem { if (!isComboChart(chartOptions.type)) { return seriesItem; } const { type, stacking } = seriesItem; return { ...seriesItem, dataLabels: { style: getLabelStyle(type, stacking), }, }; } function countMeasuresInSeries(series: ISeriesItem[]): number[] { return series.reduce( (result: number[], seriesItem: ISeriesItem) => { result[seriesItem.yAxis] += 1; return result; }, [0, 0], ); } /** * For y axis having one series, this series should be removed stacking config * @param series */ export function getSanitizedStackingForSeries(series: ISeriesItem[]): ISeriesItem[] { const [primaryMeasuresNum, secondaryMeasuresNum] = countMeasuresInSeries(series); /** * stackMeasures is applied for both measures in each axis * stackMeasuresToPercent is applied for * - [measures on primary y-axis only] or * - [measures on secondary y-axis only] or * - [applied for measures on primary y-axis + ignore for measures on secondary y-axis] */ // has measures on both [primary y-axis] and [secondary y-axis] if (primaryMeasuresNum > 0 && secondaryMeasuresNum > 0) { return series.map((seriesItem: ISeriesItem) => { // seriesItem is on [secondary y-axis] if (seriesItem.yAxis === 1) { return { ...seriesItem, stack: null as number, // reset stackMeasuresToPercent in this case (stacking: PERCENT_STACK) stacking: seriesItem.stacking ? NORMAL_STACK : (null as string), }; } else { return seriesItem; } }); } // has [measures on primary y-axis only] or [measures on secondary y-axis only] return series; } function getSeriesConfiguration( chartOptions: IChartOptions, config: any, stackMeasures: boolean, stackMeasuresToPercent: boolean, ): { series: ISeriesItem[] } { const { series } = config; const handlers = [ partial(handleStackMeasure, stackMeasures), partial(handleStackMeasuresToPercent, stackMeasuresToPercent), partial(handleDualAxis, chartOptions), partial(handleLabelStyle, chartOptions), ]; // get series with stacking config const seriesWithStackingConfig = series.map((seriesItem: ISeriesItem) => handlers.reduce((result, handler) => handler(result), seriesItem), ); return { series: getSanitizedStackingForSeries(seriesWithStackingConfig), }; } export function getYAxisConfiguration( chartOptions: IChartOptions, config: any, chartConfig: IChartConfig, ): IYAxisConfig { const type = getPrimaryChartType(chartOptions); const { yAxis } = config; const { stackMeasuresToPercent = false } = chartConfig; // only support column char // bar chart disables stack labels by default if (!isColumnChart(type)) { return {}; } const labelsVisible: IDataLabelsVisible = get(chartConfig, "dataLabels.visible"); const { enabled: dataLabelEnabled } = getLabelsVisibilityConfig(labelsVisible); // enable by default or follow dataLabels.visible config const stackLabelConfig = isNil(dataLabelEnabled) || dataLabelEnabled; const yAxisWithStackLabel = yAxis.map((axis: IHighChartAxis, index: number) => { // disable stack labels for primary Y axis when there is 'Stack to 100%' on const stackLabelEnabled = (index !== 0 || !stackMeasuresToPercent) && stackLabelConfig; return { ...axis, stackLabels: { enabled: stackLabelEnabled, }, }; }); return { yAxis: yAxisWithStackLabel }; } /** * Set config to highchart for 'Stack Measures' and 'Stack to 100%' * @param chartOptions * @param config * @param chartConfig */ export function getStackMeasuresConfiguration( chartOptions: IChartOptions, config: any, chartConfig: IChartConfig, ): IStackMeasuresConfig { const { stackMeasures = false, stackMeasuresToPercent = false } = chartConfig; const canStackInPercent = canComboChartBeStackedInPercent(config.series); if (!stackMeasures && !stackMeasuresToPercent) { return {}; } return { ...getSeriesConfiguration( chartOptions, config, stackMeasures, stackMeasuresToPercent && canStackInPercent, ), ...getYAxisConfiguration(chartOptions, config, chartConfig), }; } /** * Add style to X axis in case of 'grouped-categories' * @param chartOptions * @param config */ export function getParentAttributeConfiguration(chartOptions: IChartOptions, config: any) { const { type } = chartOptions; const { xAxis } = config; const xAxisItem = xAxis[0]; // expect only one X axis // parent attribute in X axis const parentAttributeOptions = {}; // only apply font-weight to parent label set(parentAttributeOptions, "style", { fontWeight: "bold", }); if (isInvertedChartType(type)) { // distance more 5px for two groups of attributes for bar chart set(parentAttributeOptions, "x", -5); } // 'groupedOptions' is custom property in 'grouped-categories' plugin set(xAxisItem, "labels.groupedOptions", [parentAttributeOptions]); return { xAxis: [xAxisItem] }; } export function setDrillConfigToXAxis(drillConfig: IDrillConfig) { return { xAxis: [{ drillConfig }] }; } /** * Format labels in Y axis from '0 - 100' to '0% - 100%' * Only applied when measure/series in Y axis more than one * @param chartOptions * @param _config * @param chartConfig */ export function getShowInPercentConfiguration( chartOptions: IChartOptions, config: any = {}, chartConfig: IChartConfig, ) { const { stackMeasuresToPercent = false, primaryChartType } = chartConfig; const canStackInPercent = canComboChartBeStackedInPercent(config.series); if (!canStackInPercent || !stackMeasuresToPercent || isLineChart(primaryChartType)) { return {}; } const { yAxes = [], type } = chartOptions; const percentageFormatter = partial(formatAsPercent, 1); // suppose that max number of y axes is 2 // percentage format only supports primary axis const yAxis = yAxes.map((axis: IAxis, index: number) => { if (index !== 0 || (isComboChart(type) && !isPrimaryYAxis(axis))) { return {}; } return { labels: { formatter: percentageFormatter, }, }; }); return { yAxis }; } /** * Convert [0, 1] to [0, 100], it's needed by highchart * Only applied to primary Y axis * @param _chartOptions * @param config * @param chartConfig */ export function convertMinMaxFromPercentToNumber( _chartOptions: IChartOptions, config: any, chartConfig: IChartConfig, ) { const { stackMeasuresToPercent = false } = chartConfig; if (!stackMeasuresToPercent) { return {}; } const { yAxis: yAxes = [] as any[] } = config; const yAxis = yAxes.map((axis: any, _: number, axes: IAxis[]) => { const { min, max } = axis; const newAxis = {}; if (!isNil(min)) { set(newAxis, "min", min * 100); } if (!isNil(max)) { set(newAxis, "max", max * 100); } const numberOfAxes = axes.length; if (numberOfAxes === 1) { return newAxis; } const { opposite = false } = axis; return opposite ? {} : newAxis; }); return { yAxis }; } export default function getOptionalStackingConfiguration( chartOptions: IChartOptions, config: any, chartConfig: IChartConfig = {}, drillConfig?: IDrillConfig, ) { const { type } = chartOptions; return includes(supportedStackingAttributesChartTypes, type) ? merge( {}, setDrillConfigToXAxis(drillConfig), getParentAttributeConfiguration(chartOptions, config), getStackMeasuresConfiguration(chartOptions, config, chartConfig), getShowInPercentConfiguration(chartOptions, config, chartConfig), convertMinMaxFromPercentToNumber(chartOptions, config, chartConfig), ) : {}; }