| /** |
| * 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, |
| zoomable, |
| } = 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), |
| opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, |
| borderColor: colorFn(groupbyLabel, sliceId), |
| }, |
| }; |
| }); |
| }) |
| .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 |
| ? `<p><strong>${sanitizeHtml(outlierName)}</strong></p>` |
| : ''; |
| return `${headline}${numberFormatter(stats)}`; |
| }, |
| }, |
| itemStyle: { |
| color: colorFn(groupbyLabel, sliceId), |
| opacity: isFiltered |
| ? OpacityEnum.SemiTransparent |
| : OpacityEnum.NonTransparent, |
| }, |
| }; |
| }), |
| ) |
| .flat(2); |
| |
| const labelMap = data.reduce((acc: Record<string, string[]>, 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<string, number>, 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 |
| ? `<p><strong>${sanitizeHtml(name)}</strong></p>` |
| : ''; |
| 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('<br/>'); |
| }, |
| }, |
| }, |
| // @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, |
| toolbox: { |
| show: zoomable, |
| feature: { |
| dataZoom: { |
| title: { |
| zoom: 'zoom area', |
| back: 'restore zoom', |
| }, |
| }, |
| }, |
| }, |
| dataZoom: zoomable |
| ? [ |
| { |
| type: 'inside', |
| zoomOnMouseWheel: false, |
| moveOnMouseWheel: true, |
| }, |
| ] |
| : [], |
| }; |
| |
| return { |
| formData, |
| width, |
| height, |
| echartOptions, |
| setDataMask, |
| emitCrossFilters, |
| labelMap, |
| groupby, |
| selectedValues, |
| onContextMenu, |
| refs, |
| coltypeMapping, |
| }; |
| } |