/** * 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, getColumnLabel, getMetricLabel, getNumberFormatter, getTimeFormatter, } from '@superset-ui/core'; import type { EChartsCoreOption } from 'echarts/core'; import type { BoxplotSeriesOption } from 'echarts/charts'; import type { CallbackDataParams } from 'echarts/types/src/util/types'; import { BoxPlotChartTransformedProps, BoxPlotQueryFormData, EchartsBoxPlotChartProps, } from './types'; import { extractGroupbyLabel, getColtypesMapping, sanitizeHtml, } from '../utils/series'; import { convertInteger } from '../utils/convertInteger'; import { defaultGrid, defaultYAxis } from '../defaults'; import { getPadding } from '../Timeseries/transformers'; import { OpacityEnum } from '../constants'; import { getDefaultTooltip } from '../utils/tooltip'; import { Refs } from '../types'; export default function transformProps( chartProps: EchartsBoxPlotChartProps, ): BoxPlotChartTransformedProps { const { width, height, formData, hooks, filterState, queriesData, inContextMenu, emitCrossFilters, } = chartProps; const { data = [] } = queriesData[0]; const { setDataMask = () => {}, onContextMenu } = hooks; const coltypeMapping = getColtypesMapping(queriesData[0]); const { colorScheme, groupby = [], metrics = [], numberFormat, dateFormat, xTicksLayout, legendOrientation = 'top', xAxisTitle, yAxisTitle, xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, sliceId, } = formData as BoxPlotQueryFormData; const refs: Refs = {}; const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const numberFormatter = getNumberFormatter(numberFormat); const metricLabels = metrics.map(getMetricLabel); const groupbyLabels = groupby.map(getColumnLabel); const transformedData = data .map((datum: any) => { const groupbyLabel = extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping, timeFormatter: getTimeFormatter(dateFormat), }); return metricLabels.map(metric => { const name = metricLabels.length === 1 ? groupbyLabel : `${groupbyLabel}, ${metric}`; const isFiltered = filterState.selectedValues && !filterState.selectedValues.includes(name); return { name, value: [ datum[`${metric}__min`], datum[`${metric}__q1`], datum[`${metric}__median`], datum[`${metric}__q3`], datum[`${metric}__max`], datum[`${metric}__mean`], datum[`${metric}__count`], datum[`${metric}__outliers`], ], itemStyle: { color: colorFn(groupbyLabel, sliceId, colorScheme), opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, borderColor: colorFn(groupbyLabel, sliceId, colorScheme), }, }; }); }) .flatMap(row => row); const outlierData = data .map(datum => metricLabels.map(metric => { const groupbyLabel = extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping, timeFormatter: getTimeFormatter(dateFormat), }); const name = metricLabels.length === 1 ? groupbyLabel : `${groupbyLabel}, ${metric}`; // Outlier data is a nested array of numbers (uncommon, therefore no need to add to DataRecordValue) const outlierDatum = (datum[`${metric}__outliers`] || []) as number[]; const isFiltered = filterState.selectedValues && !filterState.selectedValues.includes(name); return { name: 'outlier', type: 'scatter', data: outlierDatum.map(val => [name, val]), tooltip: { ...getDefaultTooltip(refs), formatter: (param: { data: [string, number] }) => { const [outlierName, stats] = param.data; const headline = groupbyLabels.length ? `

${sanitizeHtml(outlierName)}

` : ''; return `${headline}${numberFormatter(stats)}`; }, }, itemStyle: { color: colorFn(groupbyLabel, sliceId, colorScheme), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, }, }; }), ) .flat(2); const labelMap = data.reduce((acc: Record, datum) => { const label = extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping, timeFormatter: getTimeFormatter(dateFormat), }); return { ...acc, [label]: groupbyLabels.map(col => datum[col] as string), }; }, {}); const selectedValues = (filterState.selectedValues || []).reduce( (acc: Record, selectedValue: string) => { const index = transformedData.findIndex( ({ name }) => name === selectedValue, ); return { ...acc, [index]: selectedValue, }; }, {}, ); let axisLabel; if (xTicksLayout === '45°') axisLabel = { rotate: -45 }; else if (xTicksLayout === '90°') axisLabel = { rotate: -90 }; else if (xTicksLayout === 'flat') axisLabel = { rotate: 0 }; else if (xTicksLayout === 'staggered') axisLabel = { rotate: -45 }; else axisLabel = { show: true }; const series: BoxplotSeriesOption[] = [ { name: 'boxplot', type: 'boxplot', data: transformedData, tooltip: { ...getDefaultTooltip(refs), formatter: (param: CallbackDataParams) => { // @ts-ignore const { value, name, }: { value: [ number, number, number, number, number, number, number, number, number[], ]; name: string; } = param; const headline = name ? `

${sanitizeHtml(name)}

` : ''; const stats = [ `Max: ${numberFormatter(value[5])}`, `3rd Quartile: ${numberFormatter(value[4])}`, `Mean: ${numberFormatter(value[6])}`, `Median: ${numberFormatter(value[3])}`, `1st Quartile: ${numberFormatter(value[2])}`, `Min: ${numberFormatter(value[1])}`, `# Observations: ${value[7]}`, ]; if (value[8].length > 0) { stats.push(`# Outliers: ${value[8].length}`); } return headline + stats.join('
'); }, }, }, // @ts-ignore ...outlierData, ]; const addYAxisTitleOffset = !!yAxisTitle; const addXAxisTitleOffset = !!xAxisTitle; const chartPadding = getPadding( true, legendOrientation, addYAxisTitleOffset, false, null, addXAxisTitleOffset, yAxisTitlePosition, convertInteger(yAxisTitleMargin), convertInteger(xAxisTitleMargin), ); const echartOptions: EChartsCoreOption = { grid: { ...defaultGrid, ...chartPadding, }, xAxis: { type: 'category', data: transformedData.map(row => row.name), axisLabel, name: xAxisTitle, nameGap: convertInteger(xAxisTitleMargin), nameLocation: 'middle', }, yAxis: { ...defaultYAxis, type: 'value', axisLabel: { formatter: numberFormatter }, name: yAxisTitle, nameGap: convertInteger(yAxisTitleMargin), nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', }, tooltip: { ...getDefaultTooltip(refs), show: !inContextMenu, trigger: 'item', axisPointer: { type: 'shadow', }, }, series, }; return { formData, width, height, echartOptions, setDataMask, emitCrossFilters, labelMap, groupby, selectedValues, onContextMenu, refs, coltypeMapping, }; }