UNPKG

@gooddata/react-components

Version:

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

1,184 lines (1,018 loc) • 38.9 kB
// (C) 2007-2020 GoodData Corporation import noop = require("lodash/noop"); import isString = require("lodash/isString"); import set = require("lodash/set"); import get = require("lodash/get"); import merge = require("lodash/merge"); import map = require("lodash/map"); import partial = require("lodash/partial"); import isEmpty = require("lodash/isEmpty"); import compact = require("lodash/compact"); import cloneDeep = require("lodash/cloneDeep"); import every = require("lodash/every"); import isNil = require("lodash/isNil"); import pickBy = require("lodash/pickBy"); import * as numberJS from "@gooddata/numberjs"; import * as cx from "classnames"; import { styleVariables } from "../../styles/variables"; import { supportedDualAxesChartTypes, supportedTooltipFollowPointerChartTypes } from "../chartOptionsBuilder"; import { VisualizationTypes, ChartType } from "../../../../constants/visualizationTypes"; import { IDataLabelsVisible, IChartConfig, IAxis, IChartOptions, ISeriesItem, IPointData, } from "../../../../interfaces/Config"; import { percentFormatter } from "../../../../helpers/utils"; import { formatAsPercent, getLabelStyle, getLabelsVisibilityConfig, isInPercent } from "./dataLabelsHelpers"; import { HOVER_BRIGHTNESS, MINIMUM_HC_SAFE_BRIGHTNESS } from "./commonConfiguration"; import { AXIS_LINE_COLOR, getLighterColor } from "../../utils/color"; import { isBarChart, isColumnChart, isBulletChart, isOneOfTypes, isAreaChart, isRotationInRange, isHeatmap, isScatterPlot, isBubbleChart, isComboChart, isInvertedChartType, } from "../../utils/common"; import { TOOLTIP_MAX_WIDTH, isTooltipShownInFullScreen, getTooltipContentWidth } from "../../chart/tooltip"; import { shouldFollowPointer } from "../../../visualizations/chart/highcharts/helpers"; import { shouldStartOnTick, shouldEndOnTick, shouldXAxisStartOnTickOnBubbleScatter, shouldYAxisStartOnTickOnBubbleScatter, } from "../highcharts/helpers"; import getOptionalStackingConfiguration from "./getOptionalStackingConfiguration"; import { IDrillConfig } from "../../../../interfaces/DrillEvents"; import { getZeroAlignConfiguration } from "./getZeroAlignConfiguration"; import { canComboChartBeStackedInPercent } from "../chartOptions/comboChartOptions"; import { getAxisNameConfiguration } from "./getAxisNameConfiguration"; import { getChartAlignmentConfiguration } from "./getChartAlignmentConfiguration"; import { getAxisLabelConfigurationForDualBarChart } from "./getAxisLabelConfigurationForDualBarChart"; const { stripColors, numberFormat }: any = numberJS; const EMPTY_DATA: any = { categories: [], series: [] }; const ALIGN_LEFT = "left"; const ALIGN_RIGHT = "right"; const ALIGN_CENTER = "center"; const TOOLTIP_ARROW_OFFSET = 23; const TOOLTIP_INVERTED_CHART_VERTICAL_OFFSET = 5; const TOOLTIP_VERTICAL_OFFSET = 14; export const TOOLTIP_PADDING = 24; // padding of tooltip container - defined by CSS export const TOOLTIP_VIEWPORT_MARGIN_TOP = 20; const BAR_COLUMN_TOOLTIP_TOP_OFFSET = 8; const BAR_COLUMN_TOOLTIP_LEFT_OFFSET = 5; const HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET = 16; const escapeAngleBrackets = (str: any) => str && str.replace(/</g, "&lt;").replace(/>/g, "&gt;"); function getTitleConfiguration(chartOptions: IChartOptions) { const { yAxes = [], xAxes = [] } = chartOptions; const yAxis = yAxes.map((axis: IAxis) => axis ? { title: { text: escapeAngleBrackets(get(axis, "label", "")), }, } : {}, ); const xAxis = xAxes.map((axis: IAxis) => axis ? { title: { text: escapeAngleBrackets(get(axis, "label", "")), }, } : {}, ); return { yAxis, xAxis, }; } export function formatOverlappingForParentAttribute(category: any) { // category is passed from 'grouped-categories' which is npm highcharts plug-in if (!category) { return formatOverlapping.call(this); } const categoriesCount = get(this, "axis.categoriesTree", []).length; if (categoriesCount === 1) { // Let the width be auto to make sure "this.value" is displayed on screen return `<div style="overflow: hidden; text-overflow: ellipsis">${this.value}</div>`; } const chartHeight = get(this, "axis.chart.chartHeight", 1); const width = Math.floor(chartHeight / categoriesCount); const pixelOffset = 40; // parent attribute should have more space than its children const finalWidth = Math.max(0, width - pixelOffset); return `<div style="width: ${finalWidth}px; overflow: hidden; text-overflow: ellipsis">${ this.value }</div>`; } export function formatOverlapping() { const categoriesCount = get(this, "axis.categories", []).length; if (categoriesCount === 1) { // Let the width be auto to make sure "this.value" is displayed on screen return `<div align="center" style="overflow: hidden; text-overflow: ellipsis">${this.value}</div>`; } const chartHeight = get(this, "chart.chartHeight", 1); const width = Math.floor(chartHeight / categoriesCount); const pixelOffset = 20; const finalWidth = Math.max(0, width - pixelOffset); return ( `<div align="center" style="width: ${finalWidth}px; overflow: hidden; text-overflow: ellipsis">` + this.value + "</div>" ); } function hideOverlappedLabels(chartOptions: IChartOptions) { const rotation = Number(get(chartOptions, "xAxisProps.rotation", "0")); // Set only for bar chart and labels are rotated by 90 const isInvertedChart = isInvertedChartType(chartOptions.type); if (isInvertedChart && isRotationInRange(rotation, 75, 105)) { const { xAxes = [], isViewByTwoAttributes } = chartOptions; return { xAxis: xAxes.map((axis: any) => axis ? { labels: { useHTML: true, formatter: isViewByTwoAttributes ? formatOverlappingForParentAttribute : formatOverlapping, }, } : {}, ), }; } return {}; } function getShowInPercentConfiguration(chartOptions: IChartOptions) { const { yAxes = [], xAxes = [] } = chartOptions; const percentageFormatter = partial(formatAsPercent, 100); const xAxis = xAxes.map((axis: any) => axis && isInPercent(axis.format) ? { labels: { formatter: percentageFormatter, }, } : {}, ); const yAxis = yAxes.map((axis: any) => axis && isInPercent(axis.format) ? { labels: { formatter: percentageFormatter, }, } : {}, ); return { xAxis, yAxis, }; } function getArrowAlignment(arrowPosition: any, chartWidth: any) { const minX = -TOOLTIP_ARROW_OFFSET; const maxX = chartWidth + TOOLTIP_ARROW_OFFSET; if (arrowPosition + TOOLTIP_MAX_WIDTH / 2 > maxX && arrowPosition - TOOLTIP_MAX_WIDTH / 2 > minX) { return ALIGN_RIGHT; } if (arrowPosition - TOOLTIP_MAX_WIDTH / 2 < minX && arrowPosition + TOOLTIP_MAX_WIDTH / 2 < maxX) { return ALIGN_LEFT; } return ALIGN_CENTER; } const getTooltipHorizontalStartingPosition = (arrowPosition: any, chartWidth: any, tooltipWidth: any) => { switch (getArrowAlignment(arrowPosition, chartWidth)) { case ALIGN_RIGHT: return arrowPosition - tooltipWidth + TOOLTIP_ARROW_OFFSET; case ALIGN_LEFT: return arrowPosition - TOOLTIP_ARROW_OFFSET; default: return arrowPosition - tooltipWidth / 2; } }; function getArrowHorizontalPosition(chartType: any, stacking: any, dataPointEnd: any, dataPointHeight: any) { if (isBarChart(chartType) && stacking) { return dataPointEnd - dataPointHeight / 2; } return dataPointEnd; } function getDataPointEnd(chartType: any, isNegative: any, endPoint: any, height: any, stacking: any) { return isBarChart(chartType) && isNegative && stacking ? endPoint + height : endPoint; } function getDataPointStart(chartType: any, isNegative: any, endPoint: any, height: any, stacking: any) { return isColumnChart(chartType) && isNegative && stacking ? endPoint - height : endPoint; } function getTooltipVerticalOffset(chartType: any, stacking: any, point: any) { if (isColumnChart(chartType) && (stacking || point.negative)) { return 0; } if (isInvertedChartType(chartType)) { return TOOLTIP_INVERTED_CHART_VERTICAL_OFFSET; } return TOOLTIP_VERTICAL_OFFSET; } export function getTooltipPositionInChartContainer( chartType: string, stacking: string, labelWidth: number, labelHeight: number, point: IPointData, ) { const dataPointEnd = getDataPointEnd(chartType, point.negative, point.plotX, point.h, stacking); const arrowPosition = getArrowHorizontalPosition(chartType, stacking, dataPointEnd, point.h); const chartWidth = this.chart.plotWidth; const tooltipHorizontalStartingPosition = getTooltipHorizontalStartingPosition( arrowPosition, chartWidth, labelWidth, ); const verticalOffset = getTooltipVerticalOffset(chartType, stacking, point); const dataPointStart = getDataPointStart(chartType, point.negative, point.plotY, point.h, stacking); return { x: this.chart.plotLeft + tooltipHorizontalStartingPosition, y: this.chart.plotTop + dataPointStart - (labelHeight + verticalOffset), }; } function getHighchartTooltipTopOffset(chartType: string): number { if ( isBarChart(chartType) || isColumnChart(chartType) || isBulletChart(chartType) || isComboChart(chartType) ) { return BAR_COLUMN_TOOLTIP_TOP_OFFSET; } return HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET; } function getHighchartTooltipLeftOffset(chartType: string): number { if ( isBarChart(chartType) || isColumnChart(chartType) || isBulletChart(chartType) || isComboChart(chartType) ) { return BAR_COLUMN_TOOLTIP_LEFT_OFFSET; } return HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET; } export function getTooltipPositionInViewPort( chartType: string, stacking: string, labelWidth: number, labelHeight: number, point: IPointData, ) { const { x, y } = getTooltipPositionInChartContainer.call( this, chartType, stacking, labelWidth, labelHeight, point, ); const { top: containerTop, left: containerLeft } = this.chart.container.getBoundingClientRect(); const leftOffset = pageXOffset + containerLeft - getHighchartTooltipLeftOffset(chartType); const topOffset = pageYOffset + containerTop - getHighchartTooltipTopOffset(chartType); const posX = isTooltipShownInFullScreen() ? leftOffset : leftOffset + x; const posY = topOffset + y; const minPosY = TOOLTIP_VIEWPORT_MARGIN_TOP - TOOLTIP_PADDING + pageYOffset; const posYLimited = posY < minPosY ? minPosY : posY; return { x: posX, y: posYLimited, }; } function formatTooltip(tooltipCallback: any) { const { chart } = this.series; const { color: pointColor } = this.point; const chartWidth = chart.spacingBox.width; const isFullScreenTooltip = isTooltipShownInFullScreen(); const maxTooltipContentWidth = getTooltipContentWidth(isFullScreenTooltip, chartWidth, TOOLTIP_MAX_WIDTH); // when brushing, do not show tooltip if (chart.mouseIsDown) { return false; } const strokeStyle = pointColor ? `border-top-color: ${pointColor};` : ""; const tooltipStyle = isFullScreenTooltip ? `width: ${maxTooltipContentWidth}px;` : ""; // null disables whole tooltip const tooltipContent: string = tooltipCallback(this.point, maxTooltipContentWidth, this.percentage); return tooltipContent !== null ? `<div class="hc-tooltip gd-viz-tooltip" style="${tooltipStyle}"> <span class="stroke gd-viz-tooltip-stroke" style="${strokeStyle}"></span> <div class="content gd-viz-tooltip-content" style="max-width: ${maxTooltipContentWidth}px;"> ${tooltipContent} </div> </div>` : null; } function formatLabel(value: any, format: any, config: IChartConfig = {}) { // no labels for missing values if (isNil(value)) { return null; } const stripped = stripColors(format || ""); const { separators } = config; return escapeAngleBrackets(String(numberFormat(value, stripped, undefined, separators))); } function labelFormatter(config?: IChartConfig) { return formatLabel(this.y, get(this, "point.format"), config); } export function percentageDataLabelFormatter(config?: IChartConfig): string { // suppose that chart has one Y axis by default const isSingleAxis = get(this, "series.chart.yAxis.length", 1) === 1; const isPrimaryAxis = !get(this, "series.yAxis.opposite", false); // only format data labels to percentage for // * left or right axis on single axis chart, or // * primary axis on dual axis chart if (this.percentage && (isSingleAxis || isPrimaryAxis)) { return percentFormatter(this.percentage); } return labelFormatter.call(this, config); } function labelFormatterHeatmap(options: any) { return formatLabel(this.point.value, options.formatGD, options.config); } function level1LabelsFormatter(config?: IChartConfig) { return `${get(this, "point.name")} (${formatLabel( get(this, "point.node.val"), get(this, "point.format"), config, )})`; } function level2LabelsFormatter(config?: IChartConfig) { return `${get(this, "point.name")} (${formatLabel( get(this, "point.value"), get(this, "point.format"), config, )})`; } function labelFormatterBubble(config?: IChartConfig) { const value = get(this, "point.z"); if (isNil(value) || isNaN(value)) { return null; } const xAxisMin = get(this, "series.xAxis.min"); const xAxisMax = get(this, "series.xAxis.max"); const yAxisMin = get(this, "series.yAxis.min"); const yAxisMax = get(this, "series.yAxis.max"); if ( (!isNil(xAxisMax) && this.x > xAxisMax) || (!isNil(xAxisMin) && this.x < xAxisMin) || (!isNil(yAxisMax) && this.y > yAxisMax) || (!isNil(yAxisMin) && this.y < yAxisMin) ) { return null; } else { return formatLabel(value, get(this, "point.format"), config); } } function labelFormatterScatter() { const name = get(this, "point.name"); if (name) { return escapeAngleBrackets(name); } return null; } // check whether series contains only positive values, not consider nulls function hasOnlyPositiveValues(series: any, x: any) { return every(series, (seriesItem: any) => { const dataPoint = seriesItem.yData[x]; return dataPoint !== null && dataPoint >= 0; }); } function stackLabelFormatter(config?: IChartConfig) { // show labels: always for negative, // without negative values or with non-zero total for positive const showStackLabel = this.isNegative || hasOnlyPositiveValues(this.axis.series, this.x) || this.total !== 0; return showStackLabel ? formatLabel(this.total, get(this, "axis.userOptions.defaultFormat"), config) : null; } function getTooltipConfiguration(chartOptions: IChartOptions) { const tooltipAction = get(chartOptions, "actions.tooltip"); const chartType = chartOptions.type; const { stacking } = chartOptions; const followPointer = isOneOfTypes(chartType, supportedTooltipFollowPointerChartTypes) ? { followPointer: shouldFollowPointer(chartOptions) } : {}; return tooltipAction ? { tooltip: { borderWidth: 0, borderRadius: 0, shadow: false, useHTML: true, outside: true, positioner: partial(getTooltipPositionInViewPort, chartType, stacking), formatter: partial(formatTooltip, tooltipAction), ...followPointer, }, } : {}; } function getTreemapLabelsConfiguration( isMultiLevel: boolean, style: any, config?: IChartConfig, labelsConfig?: object, ) { const smallLabelInCenter = { dataLabels: { enabled: true, padding: 2, formatter: partial(level2LabelsFormatter, config), allowOverlap: false, style, ...labelsConfig, }, }; if (isMultiLevel) { return { dataLabels: { ...labelsConfig, }, levels: [ { level: 1, dataLabels: { enabled: true, align: "left", verticalAlign: "top", padding: 5, style: { ...style, fontSize: "14px", }, formatter: partial(level1LabelsFormatter, config), allowOverlap: false, ...labelsConfig, }, }, { level: 2, ...smallLabelInCenter, }, ], }; } else { return { dataLabels: { ...labelsConfig, }, levels: [ { level: 1, ...smallLabelInCenter, }, ], }; } } function getLabelsConfiguration(chartOptions: IChartOptions, _config: any, chartConfig?: IChartConfig) { const { stacking, yAxes = [], type } = chartOptions; const labelsVisible: IDataLabelsVisible = get(chartConfig, "dataLabels.visible"); const labelsConfig = getLabelsVisibilityConfig(labelsVisible); const style = getLabelStyle(type, stacking); const yAxis = yAxes.map((axis: any) => ({ defaultFormat: get(axis, "format"), })); const series: ISeriesItem[] = get(chartOptions, "data.series", []); const canStackInPercent = canComboChartBeStackedInPercent(series); const { stackMeasuresToPercent = false } = chartConfig || {}; // only applied to bar, column, dual axis and area chart const dataLabelFormatter = stackMeasuresToPercent && canStackInPercent ? percentageDataLabelFormatter : labelFormatter; const DEFAULT_LABELS_CONFIG = { formatter: partial(labelFormatter, chartConfig), style, allowOverlap: false, ...labelsConfig, }; return { plotOptions: { gdcOptions: { dataLabels: { visible: labelsVisible, }, }, bar: { dataLabels: { ...DEFAULT_LABELS_CONFIG, formatter: partial(dataLabelFormatter, chartConfig), }, }, column: { dataLabels: { ...DEFAULT_LABELS_CONFIG, formatter: partial(dataLabelFormatter, chartConfig), }, }, heatmap: { dataLabels: { formatter: labelFormatterHeatmap, config: chartConfig, ...labelsConfig, }, }, treemap: { ...getTreemapLabelsConfiguration(!!stacking, style, chartConfig, labelsConfig), }, line: { dataLabels: DEFAULT_LABELS_CONFIG, }, area: { dataLabels: { ...DEFAULT_LABELS_CONFIG, formatter: partial(dataLabelFormatter, chartConfig), }, }, scatter: { dataLabels: { ...DEFAULT_LABELS_CONFIG, formatter: partial(labelFormatterScatter, chartConfig), }, }, bubble: { dataLabels: { ...DEFAULT_LABELS_CONFIG, formatter: partial(labelFormatterBubble, chartConfig), }, }, pie: { dataLabels: { ...DEFAULT_LABELS_CONFIG, verticalAlign: "middle", }, }, }, yAxis, }; } function getStackingConfiguration(chartOptions: IChartOptions, _config: any, chartConfig?: IChartConfig) { const { stacking, yAxes = [], type } = chartOptions; let labelsConfig = {}; if (isColumnChart(type)) { const labelsVisible: IDataLabelsVisible = get(chartConfig, "dataLabels.visible"); labelsConfig = getLabelsVisibilityConfig(labelsVisible); } const yAxis = yAxes.map(() => ({ stackLabels: { ...labelsConfig, formatter: partial(stackLabelFormatter, chartConfig), }, })); let connectNulls = {}; if (stacking && isAreaChart(type)) { connectNulls = { connectNulls: true, }; } return stacking ? { plotOptions: { series: { stacking, // this stacking config will be applied to all series ...connectNulls, }, }, yAxis, } : {}; } function getSeries(series: any) { return series.map((seriesItem: any) => { const item = cloneDeep(seriesItem); // Escaping is handled by highcharts so we don't want to provide escaped input. // With one exception, though. Highcharts supports defining styles via // for example <b>...</b> and parses that from series name. // So to avoid this parsing, escape only < and > to &lt; and &gt; // which is understood by highcharts correctly item.name = item.name && escapeAngleBrackets(item.name); // Escape data items for pie chart item.data = item.data.map((dataItem: any) => { if (!dataItem) { return dataItem; } return { ...dataItem, name: escapeAngleBrackets(dataItem.name), }; }); return item; }); } function getHeatmapDataConfiguration(chartOptions: IChartOptions) { const data = chartOptions.data || EMPTY_DATA; const series = data.series; const categories = data.categories; return { series, xAxis: [ { categories: categories[0] || [], }, ], yAxis: [ { categories: categories[1] || [], }, ], colorAxis: { dataClasses: get(chartOptions, "colorAxis.dataClasses", []), }, }; } export function escapeCategories(dataCategories: any) { return map(dataCategories, (category: any) => { return isString(category) ? escapeAngleBrackets(category) : { name: escapeAngleBrackets(category.name), categories: map(category.categories, escapeAngleBrackets), }; }); } function getDataConfiguration(chartOptions: IChartOptions) { const data = chartOptions.data || EMPTY_DATA; const series = getSeries(data.series); const { type } = chartOptions; switch (type) { case VisualizationTypes.SCATTER: case VisualizationTypes.BUBBLE: return { series, }; case VisualizationTypes.HEATMAP: return getHeatmapDataConfiguration(chartOptions); } const categories = escapeCategories(data.categories); return { series, xAxis: [ { categories, }, ], }; } function lineSeriesMapFn(seriesOrig: any) { const series = cloneDeep(seriesOrig); if (series.isDrillable) { set(series, "marker.states.hover.fillColor", getLighterColor(series.color, HOVER_BRIGHTNESS)); } else { set(series, "states.hover.halo.size", 0); } return series; } function barSeriesMapFn(seriesOrig: any) { const series = cloneDeep(seriesOrig); set(series, "states.hover.brightness", HOVER_BRIGHTNESS); set(series, "states.hover.enabled", series.isDrillable); return series; } function getHeatMapHoverColor(config: any) { const dataClasses = get(config, ["colorAxis", "dataClasses"], null); let resultColor = "rgb(210,210,210)"; if (dataClasses) { if (dataClasses.length === 1) { resultColor = dataClasses[0].color; } else if (dataClasses.length > 1) { resultColor = dataClasses[1].color; } } return getLighterColor(resultColor, 0.2); } function getHoverStyles({ type }: any, config: any) { let seriesMapFn = noop; switch (type) { default: throw new Error(`Undefined chart type "${type}".`); case VisualizationTypes.LINE: case VisualizationTypes.SCATTER: case VisualizationTypes.AREA: case VisualizationTypes.BUBBLE: seriesMapFn = lineSeriesMapFn; break; case VisualizationTypes.BAR: case VisualizationTypes.COLUMN: case VisualizationTypes.BULLET: case VisualizationTypes.FUNNEL: seriesMapFn = barSeriesMapFn; break; case VisualizationTypes.HEATMAP: seriesMapFn = (seriesOrig, config) => { const series = cloneDeep(seriesOrig); const color = getHeatMapHoverColor(config); set(series, "states.hover.color", color); set(series, "states.hover.enabled", series.isDrillable); return series; }; break; case VisualizationTypes.COMBO: case VisualizationTypes.COMBO2: seriesMapFn = seriesOrig => { const { type } = seriesOrig; if (type === "line") { return lineSeriesMapFn(seriesOrig); } return barSeriesMapFn(seriesOrig); }; break; case VisualizationTypes.PIE: case VisualizationTypes.DONUT: case VisualizationTypes.TREEMAP: seriesMapFn = seriesOrig => { const series = cloneDeep(seriesOrig); return { ...series, data: series.data.map((dataItemOrig: any) => { const dataItem = cloneDeep(dataItemOrig); const drilldown = get(dataItem, "drilldown"); set( dataItem, "states.hover.brightness", drilldown ? HOVER_BRIGHTNESS : MINIMUM_HC_SAFE_BRIGHTNESS, ); if (!drilldown) { set(dataItem, "halo.size", 0); // see plugins/pointHalo.js } return dataItem; }), }; }; break; } return { series: config.series.map((item: any) => seriesMapFn(item, config)), plotOptions: { ...[ VisualizationTypes.LINE, VisualizationTypes.AREA, VisualizationTypes.SCATTER, VisualizationTypes.BUBBLE, ].reduce( (conf: any, key) => ({ ...conf, [key]: { point: { events: { // Workaround // from Highcharts 5.0.0 cursor can be set by using 'className' for individual data items mouseOver() { if (this.drilldown) { this.graphic.element.style.cursor = "pointer"; } }, }, }, }, }), {}, ), }, }; } function getGridConfiguration(chartOptions: IChartOptions) { const gridEnabled = get(chartOptions, "grid.enabled", true); const { yAxes = [], xAxes = [] } = chartOptions; const config = gridEnabled ? { gridLineWidth: 1, gridLineColor: "#ebebeb" } : { gridLineWidth: 0 }; const yAxis = yAxes.map(() => config); const bothAxesGridlineCharts = [VisualizationTypes.BUBBLE, VisualizationTypes.SCATTER]; let xAxis = {}; if (isOneOfTypes(chartOptions.type, bothAxesGridlineCharts)) { xAxis = xAxes.map(() => config); } return { yAxis, xAxis, }; } export function areAxisLabelsEnabled( chartOptions: IChartOptions, axisPropsName: string, shouldCheckForEmptyCategories: boolean, ) { const data = chartOptions.data || EMPTY_DATA; const { type } = chartOptions; const categories = isHeatmap(type) ? data.categories : escapeCategories(data.categories); const visible = get(chartOptions, `${axisPropsName}.visible`, true); const labelsEnabled = get(chartOptions, `${axisPropsName}.labelsEnabled`, true); const categoriesFlag = shouldCheckForEmptyCategories ? !isEmpty(compact(categories)) : true; return { enabled: categoriesFlag && visible && labelsEnabled, }; } function shouldExpandYAxis(chartOptions: IChartOptions) { const min = get(chartOptions, "xAxisProps.min", ""); const max = get(chartOptions, "xAxisProps.max", ""); return min === "" && max === "" ? {} : { getExtremesFromAll: true }; } function getAxisLineConfiguration(chartType: ChartType, isAxisVisible: boolean) { let lineWidth; // tslint:disable-next-line prefer-conditional-expression if (isAxisVisible === false) { lineWidth = 0; } else { lineWidth = isScatterPlot(chartType) || isBubbleChart(chartType) ? 1 : undefined; } return pickBy({ AXIS_LINE_COLOR, lineWidth }, (item: any) => item !== undefined); } function getXAxisTickConfiguration(chartOptions: IChartOptions) { const { type } = chartOptions; if (isBubbleChart(type) || isScatterPlot(type)) { return { startOnTick: shouldXAxisStartOnTickOnBubbleScatter(chartOptions), endOnTick: false, }; } return {}; } function getYAxisTickConfiguration(chartOptions: IChartOptions, axisPropsKey: string) { const { type, yAxes } = chartOptions; if (isBubbleChart(type) || isScatterPlot(type)) { return { startOnTick: shouldYAxisStartOnTickOnBubbleScatter(chartOptions), }; } if (isOneOfTypes(type, supportedDualAxesChartTypes) && yAxes.length > 1) { // disable { startOnTick, endOnTick } to make gridline sync in both axes return {}; } return { startOnTick: shouldStartOnTick(chartOptions, axisPropsKey), endOnTick: shouldEndOnTick(chartOptions, axisPropsKey), }; } function getAxesConfiguration(chartOptions: IChartOptions) { const { forceDisableDrillOnAxes = false } = chartOptions; const type = chartOptions.type as ChartType; return { plotOptions: { series: { ...shouldExpandYAxis(chartOptions), }, }, yAxis: get(chartOptions, "yAxes", []).map((axis: any) => { if (!axis) { return { visible: false, }; } const opposite = get(axis, "opposite", false); const axisType: string = axis.opposite ? "secondary" : "primary"; const className: string = cx(`s-highcharts-${axisType}-yaxis`, { "gd-axis-label-drilling-disabled": forceDisableDrillOnAxes, }); const axisPropsKey = opposite ? "secondary_yAxisProps" : "yAxisProps"; // For bar chart take x axis options const min = get(chartOptions, `${axisPropsKey}.min`, ""); const max = get(chartOptions, `${axisPropsKey}.max`, ""); const visible = get(chartOptions, `${axisPropsKey}.visible`, true); const maxProp = max ? { max: Number(max) } : {}; const minProp = min ? { min: Number(min) } : {}; const rotation = get(chartOptions, `${axisPropsKey}.rotation`, "auto"); const rotationProp = rotation !== "auto" ? { rotation: -Number(rotation) } : {}; const shouldCheckForEmptyCategories = isHeatmap(type) ? true : false; const labelsEnabled = areAxisLabelsEnabled( chartOptions, axisPropsKey, shouldCheckForEmptyCategories, ); const tickConfiguration = getYAxisTickConfiguration(chartOptions, axisPropsKey); return { ...getAxisLineConfiguration(type, visible), labels: { ...labelsEnabled, style: { color: styleVariables.gdColorStateBlank, font: '12px Avenir, "Helvetica Neue", Arial, sans-serif', }, ...rotationProp, }, title: { enabled: visible, margin: 15, style: { color: styleVariables.gdColorLink, font: '14px Avenir, "Helvetica Neue", Arial, sans-serif', }, }, opposite, className, ...maxProp, ...minProp, ...tickConfiguration, }; }), xAxis: get(chartOptions, "xAxes", []).map((axis: any) => { if (!axis) { return { visible: false, }; } const opposite = get(axis, "opposite", false); const axisPropsKey = opposite ? "secondary_xAxisProps" : "xAxisProps"; const className: string = cx({ "gd-axis-label-drilling-disabled": forceDisableDrillOnAxes, }); const min = get(chartOptions, axisPropsKey.concat(".min"), ""); const max = get(chartOptions, axisPropsKey.concat(".max"), ""); const maxProp = max ? { max: Number(max) } : {}; const minProp = min ? { min: Number(min) } : {}; const isViewByTwoAttributes = get(chartOptions, "isViewByTwoAttributes", false); const visible = get(chartOptions, axisPropsKey.concat(".visible"), true); const rotation = get(chartOptions, axisPropsKey.concat(".rotation"), "auto"); const rotationProp = rotation !== "auto" ? { rotation: -Number(rotation) } : {}; const shouldCheckForEmptyCategories = isScatterPlot(type) || isBubbleChart(type) ? false : true; const labelsEnabled = areAxisLabelsEnabled( chartOptions, axisPropsKey, shouldCheckForEmptyCategories, ); const tickConfiguration = getXAxisTickConfiguration(chartOptions); // for bar chart take y axis options return { ...getAxisLineConfiguration(type, visible), // hide ticks on x axis minorTickLength: 0, tickLength: 0, // padding of maximum value maxPadding: 0.05, labels: { ...labelsEnabled, style: { color: styleVariables.gdColorStateBlank, font: '12px Avenir, "Helvetica Neue", Arial, sans-serif', }, autoRotation: [-90], ...rotationProp, }, title: { // should disable X axis title when 'View By 2 attributes' enabled: visible && !isViewByTwoAttributes, margin: 10, style: { textOverflow: "ellipsis", color: styleVariables.gdColorLink, font: '14px Avenir, "Helvetica Neue", Arial, sans-serif', }, }, className, ...maxProp, ...minProp, ...tickConfiguration, }; }), }; } function getTargetCursorConfigurationForBulletChart(chartOptions: IChartOptions) { const { type, data } = chartOptions; if (!isBulletChart(type)) { return {}; } const isTargetDrillable = data.series.some( (series: ISeriesItem) => series.type === "bullet" && series.isDrillable, ); return isTargetDrillable ? { plotOptions: { bullet: { cursor: "pointer" } } } : {}; } export function getCustomizedConfiguration( chartOptions: IChartOptions, chartConfig?: IChartConfig, drillConfig?: IDrillConfig, ) { const configurators = [ getAxesConfiguration, getTitleConfiguration, getStackingConfiguration, hideOverlappedLabels, getShowInPercentConfiguration, getDataConfiguration, getTooltipConfiguration, getHoverStyles, getGridConfiguration, getLabelsConfiguration, // should be after 'getDataConfiguration' to modify 'series' // and should be after 'getStackingConfiguration' to get stackLabels config getOptionalStackingConfiguration, getZeroAlignConfiguration, getAxisNameConfiguration, getChartAlignmentConfiguration, getAxisLabelConfigurationForDualBarChart, getTargetCursorConfigurationForBulletChart, ]; const commonData = configurators.reduce((config: any, configurator: any) => { return merge(config, configurator(chartOptions, config, chartConfig, drillConfig)); }, {}); return merge({}, commonData); }