/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CategoricalColorNamespace, DataRecord, getColumnLabel, getMetricLabel, getNumberFormatter, getValueFormatter, NumberFormats, tooltipHtml, ValueFormatter, VizType, } from '@superset-ui/core'; import type { CallbackDataParams } from 'echarts/types/src/util/types'; import type { EChartsCoreOption } from 'echarts/core'; import type { FunnelSeriesOption } from 'echarts/charts'; import { DEFAULT_FORM_DATA as DEFAULT_FUNNEL_FORM_DATA, EchartsFunnelChartProps, EchartsFunnelFormData, EchartsFunnelLabelType, FunnelChartTransformedProps, PercentCalcType, } from './types'; import { extractGroupbyLabel, getChartPadding, getColtypesMapping, getLegendProps, sanitizeHtml, } from '../utils/series'; import { defaultGrid } from '../defaults'; import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; import { getDefaultTooltip } from '../utils/tooltip'; import { Refs } from '../types'; const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); export function parseParams({ params, numberFormatter, percentCalculationType = PercentCalcType.FirstStep, sanitizeName = false, }: { params: Pick; numberFormatter: ValueFormatter; percentCalculationType?: PercentCalcType; sanitizeName?: boolean; }) { const { name: rawName = '', value, percent: totalPercent, data } = params; const name = sanitizeName ? sanitizeHtml(rawName) : rawName; const formattedValue = numberFormatter(value as number); const { firstStepPercent, prevStepPercent } = data as { firstStepPercent: number; prevStepPercent: number; }; let percent; if (percentCalculationType === PercentCalcType.Total) { percent = (totalPercent ?? 0) / 100; } else if (percentCalculationType === PercentCalcType.PreviousStep) { percent = prevStepPercent ?? 0; } else { percent = firstStepPercent ?? 0; } const formattedPercent = percentFormatter(percent); return [name, formattedValue, formattedPercent]; } export default function transformProps( chartProps: EchartsFunnelChartProps, ): FunnelChartTransformedProps { const { formData, height, hooks, filterState, queriesData, width, theme, emitCrossFilters, datasource, } = chartProps; const data: DataRecord[] = queriesData[0].data || []; const detectedCurrency = queriesData[0]?.detected_currency; const coltypeMapping = getColtypesMapping(queriesData[0]); const { colorScheme, groupby, orient, sort, gap, labelLine, labelType, tooltipLabelType, legendMargin, legendOrientation, legendType, legendSort, metric = '', numberFormat, currencyFormat, showLabels, inContextMenu, showTooltipLabels, showLegend, sliceId, percentCalculationType, }: EchartsFunnelFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_FUNNEL_FORM_DATA, ...formData, }; const { currencyFormats = {}, columnFormats = {}, currencyCodeColumn, } = datasource; const refs: Refs = {}; const metricLabel = getMetricLabel(metric); const groupbyLabels = groupby.map(getColumnLabel); const keys = data.map(datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping: {} }), ); const labelMap = data.reduce((acc: Record, datum) => { const label = extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping: {}, }); return { ...acc, [label]: groupbyLabels.map(col => datum[col] as string), }; }, {}); const { setDataMask = () => {}, onContextMenu } = hooks; const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const numberFormatter = getValueFormatter( metric, currencyFormats, columnFormats, numberFormat, currencyFormat, undefined, data, currencyCodeColumn, detectedCurrency, ); const transformedData: { value: number; name: string; itemStyle: { color: string; opacity: OpacityEnum }; }[] = data.map((datum, index) => { const name = extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping: {}, }); const value = datum[metricLabel] as number; const isFiltered = filterState.selectedValues && !filterState.selectedValues.includes(name); const firstStepPercent = value / (data[0][metricLabel] as number); const prevStepPercent = index === 0 ? 1 : value / (data[index - 1][metricLabel] as number); return { value, name, itemStyle: { color: colorFn(name, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, }, firstStepPercent, prevStepPercent, }; }); const selectedValues = (filterState.selectedValues || []).reduce( (acc: Record, selectedValue: string) => { const index = transformedData.findIndex( ({ name }) => name === selectedValue, ); return { ...acc, [index]: selectedValue, }; }, {}, ); const formatter = (params: CallbackDataParams) => { const [name, formattedValue, formattedPercent] = parseParams({ params, numberFormatter, percentCalculationType, }); switch (labelType) { case EchartsFunnelLabelType.Key: return name; case EchartsFunnelLabelType.Value: return formattedValue; case EchartsFunnelLabelType.Percent: return formattedPercent; case EchartsFunnelLabelType.KeyValue: return `${name}: ${formattedValue}`; case EchartsFunnelLabelType.KeyValuePercent: return `${name}: ${formattedValue} (${formattedPercent})`; case EchartsFunnelLabelType.KeyPercent: return `${name}: ${formattedPercent}`; case EchartsFunnelLabelType.ValuePercent: return `${formattedValue} (${formattedPercent})`; default: return name; } }; const defaultLabel = { formatter, show: showLabels, color: theme.colorText, textBorderColor: theme.colorBgBase, textBorderWidth: 1, }; const series: FunnelSeriesOption[] = [ { type: VizType.Funnel, ...getChartPadding(showLegend, legendOrientation, legendMargin), animation: true, minSize: '0%', maxSize: '100%', sort, orient, gap, funnelAlign: 'center', labelLine: { show: !!labelLine }, label: { ...defaultLabel, position: labelLine ? 'outer' : 'inner', }, emphasis: { label: { show: true, fontWeight: 'bold', }, }, data: transformedData, }, ]; const echartOptions: EChartsCoreOption = { grid: { ...defaultGrid, }, tooltip: { ...getDefaultTooltip(refs), show: !inContextMenu && showTooltipLabels, trigger: 'item', formatter: (params: any) => { const [name, formattedValue, formattedPercent] = parseParams({ params, numberFormatter, percentCalculationType, }); const row = []; const enumName = EchartsFunnelLabelType[tooltipLabelType]; const title = enumName.includes('Key') ? name : undefined; if (enumName.includes('Value') || enumName.includes('Percent')) { row.push(metricLabel); } if (enumName.includes('Value')) { row.push(formattedValue); } if (enumName.includes('Percent')) { row.push(formattedPercent); } return tooltipHtml([row], title); }, }, legend: { ...getLegendProps(legendType, legendOrientation, showLegend, theme), data: keys.sort((a: string, b: string) => { if (!legendSort) return 0; return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); }), }, series, }; return { formData, width, height, echartOptions, setDataMask, emitCrossFilters, labelMap, groupby, selectedValues, onContextMenu, refs, coltypeMapping, }; }