From 17792a507c7245c9e09c6eb98a774f2ef4ec8568 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Fri, 6 Oct 2023 21:08:16 +0800 Subject: [PATCH] feat(plugin-chart-echarts): Echarts Waterfall (#17906) Co-authored-by: Michael S. Molina --- .../Waterfall/Stories.tsx | 69 +++ .../plugin-chart-echarts/Waterfall/data.ts | 80 ++++ .../src/Waterfall/EchartsWaterfall.tsx | 84 ++++ .../src/Waterfall/buildQuery.ts | 29 ++ .../src/Waterfall/constants.ts | 29 ++ .../src/Waterfall/controlPanel.tsx | 142 +++++++ .../src/Waterfall/images/thumbnail.png | Bin 0 -> 77020 bytes .../src/Waterfall/index.ts | 59 +++ .../src/Waterfall/transformProps.ts | 401 ++++++++++++++++++ .../src/Waterfall/types.ts | 66 +++ .../plugins/plugin-chart-echarts/src/index.ts | 2 + .../test/Waterfall/buildQuery.test.ts | 38 ++ .../test/Waterfall/transformProps.test.ts | 121 ++++++ .../src/visualizations/presets/MainPreset.js | 4 + 14 files changed, 1124 insertions(+) create mode 100644 superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/Stories.tsx create mode 100644 superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/data.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/constants.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/Stories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/Stories.tsx new file mode 100644 index 0000000000000..6b880fd6f8c2f --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/Stories.tsx @@ -0,0 +1,69 @@ +/* + * 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 React from 'react'; +import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; +import { withKnobs } from '@storybook/addon-knobs'; +import { + EchartsWaterfallChartPlugin, + WaterfallTransformProps, +} from '@superset-ui/plugin-chart-echarts'; +import data from './data'; +import { withResizableChartDemo } from '../../../../shared/components/ResizableChartDemo'; + +new EchartsWaterfallChartPlugin() + .configure({ key: 'echarts-waterfall' }) + .register(); + +getChartTransformPropsRegistry().registerValue( + 'echarts-waterfall', + WaterfallTransformProps, +); + +export default { + title: 'Chart Plugins|plugin-chart-echarts/Waterfall', + decorators: [withKnobs, withResizableChartDemo], +}; + +export const Waterfall = ({ width, height }) => ( + +); diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/data.ts b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/data.ts new file mode 100644 index 0000000000000..e072eedc7b72f --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Waterfall/data.ts @@ -0,0 +1,80 @@ +/** + * 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. + */ +export default [ + { due_to_group: 'Facebook', period: '2020', 'SUM(decomp_volume)': 1945565.5 }, + { + due_to_group: 'Competitor TV Advertising', + period: '2019', + 'SUM(decomp_volume)': 1213252, + }, + { + due_to_group: 'Online Advertising', + period: '2018', + 'SUM(decomp_volume)': 999990, + }, + { due_to_group: 'COREBASE', period: '2017', 'SUM(decomp_volume)': 852094 }, + { due_to_group: 'COREBASE', period: '2018', 'SUM(decomp_volume)': 736576 }, + { due_to_group: 'DISPLAY', period: '2017', 'SUM(decomp_volume)': 621608 }, + { due_to_group: 'DISPLAY', period: '2018', 'SUM(decomp_volume)': 388904 }, + { due_to_group: 'Facebook', period: '2019', 'SUM(decomp_volume)': 94909 }, + { + due_to_group: 'Online Advertising', + period: '2017', + 'SUM(decomp_volume)': 81334, + }, + { due_to_group: 'Halo TV', period: '2018', 'SUM(decomp_volume)': 66828 }, + { due_to_group: 'Halo TV', period: '2017', 'SUM(decomp_volume)': 46818 }, + { + due_to_group: 'Competitor TV Advertising', + period: '2017', + 'SUM(decomp_volume)': 25252, + }, + { due_to_group: 'Facebook', period: '2017', 'SUM(decomp_volume)': 23932 }, + { due_to_group: 'DFSI', period: '2017', 'SUM(decomp_volume)': 21466 }, + { due_to_group: 'Coupons', period: '2017', 'SUM(decomp_volume)': 11160 }, + { due_to_group: 'Facebook', period: '2018', 'SUM(decomp_volume)': 9444 }, + { due_to_group: 'DFSI', period: '2019', 'SUM(decomp_volume)': 8785 }, + { + due_to_group: 'Competitive Coupons', + period: '2017', + 'SUM(decomp_volume)': 8724, + }, + { + due_to_group: 'Competitive Coupons', + period: '2019', + 'SUM(decomp_volume)': 8724, + }, + { due_to_group: 'Coupons', period: '2019', 'SUM(decomp_volume)': 2950 }, + { due_to_group: 'BB Display', period: '2019', 'SUM(decomp_volume)': 1844 }, + { due_to_group: 'BB Display', period: '2017', 'SUM(decomp_volume)': 1844 }, + { due_to_group: 'Email', period: '2017', 'SUM(decomp_volume)': 810 }, + { due_to_group: 'OTHER', period: '2017', 'SUM(decomp_volume)': 78 }, + { due_to_group: 'Email', period: '2019', 'SUM(decomp_volume)': -987000 }, + { due_to_group: 'Email', period: '2020', 'SUM(decomp_volume)': -998988 }, + { + due_to_group: 'Online Advertising', + period: '2020', + 'SUM(decomp_volume)': -1500000.7, + }, + { + due_to_group: 'Online Advertising', + period: '2019', + 'SUM(decomp_volume)': -1671652, + }, +]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx new file mode 100644 index 0000000000000..a448c9f93eab6 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx @@ -0,0 +1,84 @@ +/** + * 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 React, { useCallback } from 'react'; +import Echart from '../components/Echart'; +import { allEventHandlers } from '../utils/eventHandlers'; +import { WaterfallChartTransformedProps } from './types'; + +export default function EchartsWaterfall( + props: WaterfallChartTransformedProps, +) { + const { + height, + width, + echartOptions, + setDataMask, + labelMap, + groupby, + refs, + selectedValues, + } = props; + const handleChange = useCallback( + (values: string[]) => { + const groupbyValues = values.map(value => labelMap[value]); + + setDataMask({ + extraFormData: { + filters: + values.length === 0 + ? [] + : groupby.map((col, idx) => { + const val = groupbyValues.map(v => v[idx]); + if (val === null || val === undefined) + return { + col, + op: 'IS NULL', + }; + return { + col, + op: 'IN', + val: val as (string | number | boolean)[], + }; + }), + }, + filterState: { + value: groupbyValues.length ? groupbyValues : null, + selectedValues: values.length ? values : null, + }, + }); + }, + [setDataMask, groupby, labelMap], + ); + + const eventHandlers = { + ...allEventHandlers(props), + handleChange, + }; + + return ( + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts new file mode 100644 index 0000000000000..353ee8fa20e00 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts @@ -0,0 +1,29 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +export default function buildQuery(formData: QueryFormData) { + const { series, columns } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: columns?.length ? [series, columns] : [series], + }, + ]); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/constants.ts new file mode 100644 index 0000000000000..17b7861f80ade --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/constants.ts @@ -0,0 +1,29 @@ +/** + * 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 { t } from '@superset-ui/core'; + +export const TOTAL_MARK = t('Total'); +export const ASSIST_MARK = t('Assist'); +export const LEGEND = { + INCREASE: t('Increase'), + DECREASE: t('Decrease'), + TOTAL: t('Total'), +}; +export const TOKEN = '-'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx new file mode 100644 index 0000000000000..852f2680b3c15 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/controlPanel.tsx @@ -0,0 +1,142 @@ +/** + * 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 React from 'react'; +import { ensureIsArray, t } from '@superset-ui/core'; +import { + ControlPanelConfig, + formatSelectOptions, + getStandardizedControls, + sections, +} from '@superset-ui/chart-controls'; +import { showValueControl } from '../controls'; + +const config: ControlPanelConfig = { + controlPanelSections: [ + sections.legacyTimeseriesTime, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['series'], + ['columns'], + ['metric'], + ['adhoc_filters'], + ['row_limit'], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + ['color_scheme'], + [showValueControl], + [ + { + name: 'show_legend', + config: { + type: 'CheckboxControl', + label: t('Show legend'), + renderTrigger: true, + default: false, + description: t('Whether to display a legend for the chart'), + }, + }, + ], + [ + { + name: 'rich_tooltip', + config: { + type: 'CheckboxControl', + label: t('Rich tooltip'), + renderTrigger: true, + default: true, + description: t( + 'Shows a list of all series available at that point in time', + ), + }, + }, + ], + [
{t('X Axis')}
], + [ + { + name: 'x_axis_label', + config: { + type: 'TextControl', + label: t('X Axis Label'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'x_ticks_layout', + config: { + type: 'SelectControl', + label: t('X Tick Layout'), + choices: formatSelectOptions([ + 'auto', + 'flat', + '45°', + '90°', + 'staggered', + ]), + default: 'auto', + clearable: false, + renderTrigger: true, + description: t('The way the ticks are laid out on the X-axis'), + }, + }, + ], + [
{t('Y Axis')}
], + [ + { + name: 'y_axis_label', + config: { + type: 'TextControl', + label: t('Y Axis Label'), + renderTrigger: true, + default: '', + }, + }, + ], + ['y_axis_format'], + ], + }, + ], + controlOverrides: { + columns: { + label: t('Breakdowns'), + description: t('Defines how each series is broken down'), + multi: false, + }, + }, + formDataOverrides: formData => { + const series = getStandardizedControls() + .popAllColumns() + .filter(col => !ensureIsArray(formData.columns).includes(col)); + return { + ...formData, + series, + metric: getStandardizedControls().shiftMetric(), + }; + }, +}; + +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..91ef20f515f921cf210256da932fdf1cea1daa19 GIT binary patch literal 77020 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 literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts new file mode 100644 index 0000000000000..5242434f94c1f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts @@ -0,0 +1,59 @@ +/** + * 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 + * regardin + * g 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core'; +import buildQuery from './buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from './images/thumbnail.png'; +import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from './types'; + +export default class EchartsWaterfallChartPlugin extends ChartPlugin< + EchartsWaterfallFormData, + EchartsWaterfallChartProps +> { + /** + * The constructor is used to pass relevant metadata and callbacks that get + * registered in respective registries that are used throughout the library + * and application. A more thorough description of each property is given in + * the respective imported file. + * + * It is worth noting that `buildQuery` and is optional, and only needed for + * advanced visualizations that require either post processing operations + * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. + */ + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsWaterfall'), + metadata: new ChartMetadata({ + behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL], + credits: ['https://echarts.apache.org'], + category: t('Evolution'), + description: '', + exampleGallery: [], + name: t('Waterfall Chart'), + thumbnail, + tags: [], + }), + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts new file mode 100644 index 0000000000000..8ea8f688264bf --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts @@ -0,0 +1,401 @@ +/** + * 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, + NumberFormatter, + SupersetTheme, +} from '@superset-ui/core'; +import { EChartsOption, BarSeriesOption } from 'echarts'; +import { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + EchartsWaterfallFormData, + EchartsWaterfallChartProps, + ISeriesData, + WaterfallChartTransformedProps, +} from './types'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { defaultGrid, defaultYAxis } from '../defaults'; +import { ASSIST_MARK, LEGEND, TOKEN, TOTAL_MARK } from './constants'; +import { extractGroupbyLabel, getColtypesMapping } from '../utils/series'; +import { Refs } from '../types'; + +function formatTooltip({ + theme, + params, + numberFormatter, + richTooltip, +}: { + theme: SupersetTheme; + params: any; + numberFormatter: NumberFormatter; + richTooltip: boolean; +}) { + const htmlMaker = (params: any) => + ` +
${params.name}
+
+ ${params.marker} + + ${params.seriesName}: + + + ${numberFormatter(params.data)} + +
+ `; + + if (richTooltip) { + const [, increaseParams, decreaseParams, totalParams] = params; + if (increaseParams.data !== TOKEN || increaseParams.data === null) { + return htmlMaker(increaseParams); + } + if (decreaseParams.data !== TOKEN) { + return htmlMaker(decreaseParams); + } + if (totalParams.data !== TOKEN) { + return htmlMaker(totalParams); + } + } else if (params.seriesName !== ASSIST_MARK) { + return htmlMaker(params); + } + return ''; +} + +function transformer({ + data, + breakdown, + series, + metric, +}: { + data: DataRecord[]; + breakdown: string; + series: string; + metric: string; +}) { + // Group by series (temporary map) + const groupedData = data.reduce((acc, cur) => { + const categoryLabel = cur[series] as string; + const categoryData = acc.get(categoryLabel) || []; + categoryData.push(cur); + acc.set(categoryLabel, categoryData); + return acc; + }, new Map()); + + const transformedData: DataRecord[] = []; + + if (breakdown?.length) { + groupedData.forEach((value, key) => { + const tempValue = value; + // Calc total per period + const sum = tempValue.reduce( + (acc, cur) => acc + ((cur[metric] as number) ?? 0), + 0, + ); + // Push total per period to the end of period values array + tempValue.push({ + [series]: key, + [breakdown]: TOTAL_MARK, + [metric]: sum, + }); + transformedData.push(...tempValue); + }); + } else { + let total = 0; + groupedData.forEach((value, key) => { + const sum = value.reduce( + (acc, cur) => acc + ((cur[metric] as number) ?? 0), + 0, + ); + transformedData.push({ + [series]: key, + [metric]: sum, + }); + total += sum; + }); + transformedData.push({ + [series]: TOTAL_MARK, + [metric]: total, + }); + } + + return transformedData; +} + +export default function transformProps( + chartProps: EchartsWaterfallChartProps, +): WaterfallChartTransformedProps { + const { + width, + height, + formData, + queriesData, + hooks, + filterState, + theme, + inContextMenu, + } = chartProps; + const refs: Refs = {}; + const { data = [] } = queriesData[0]; + const coltypeMapping = getColtypesMapping(queriesData[0]); + const { setDataMask = () => {}, onContextMenu } = hooks; + const { + colorScheme, + metric = '', + columns, + series, + xTicksLayout, + showLegend, + yAxisLabel, + xAxisLabel, + yAxisFormat, + richTooltip, + showValue, + sliceId, + } = formData as EchartsWaterfallFormData; + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const numberFormatter = getNumberFormatter(yAxisFormat); + const formatter = (params: CallbackDataParams) => { + const { value, seriesName } = params; + let formattedValue = numberFormatter(value as number); + if (seriesName === LEGEND.DECREASE) { + formattedValue = `-${formattedValue}`; + } + return formattedValue; + }; + const breakdown = columns?.length ? columns : ''; + const groupby = breakdown ? [series, breakdown] : [series]; + const metricLabel = getMetricLabel(metric); + const columnLabels = groupby.map(getColumnLabel); + const columnsLabelMap = new Map(); + + const transformedData = transformer({ + data, + breakdown, + series, + metric: metricLabel, + }); + + const assistData: ISeriesData[] = []; + const increaseData: ISeriesData[] = []; + const decreaseData: ISeriesData[] = []; + const totalData: ISeriesData[] = []; + + transformedData.forEach((datum, index, self) => { + const totalSum = self.slice(0, index + 1).reduce((prev, cur, i) => { + if (breakdown?.length) { + if (cur[breakdown] !== TOTAL_MARK || i === 0) { + return prev + ((cur[metricLabel] as number) ?? 0); + } + } else if (cur[series] !== TOTAL_MARK) { + return prev + ((cur[metricLabel] as number) ?? 0); + } + return prev; + }, 0); + + const joinedName = extractGroupbyLabel({ + datum, + groupby: columnLabels, + coltypeMapping, + }); + columnsLabelMap.set( + joinedName, + columnLabels.map(col => datum[col] as string), + ); + const value = datum[metricLabel] as number; + const isNegative = value < 0; + if (datum[breakdown] === TOTAL_MARK || datum[series] === TOTAL_MARK) { + increaseData.push(TOKEN); + decreaseData.push(TOKEN); + assistData.push(TOKEN); + totalData.push(totalSum); + } else if (isNegative) { + increaseData.push(TOKEN); + decreaseData.push(Math.abs(value)); + assistData.push(totalSum); + totalData.push(TOKEN); + } else { + increaseData.push(value); + decreaseData.push(TOKEN); + assistData.push(totalSum - value); + totalData.push(TOKEN); + } + }); + + 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 }; + } + + let xAxisData: string[] = []; + if (breakdown?.length) { + xAxisData = transformedData.map(row => { + if (row[breakdown] === TOTAL_MARK) { + return row[series] as string; + } + return row[breakdown] as string; + }); + } else { + xAxisData = transformedData.map(row => row[series] as string); + } + + const barSeries: BarSeriesOption[] = [ + { + name: ASSIST_MARK, + type: 'bar', + stack: 'stack', + itemStyle: { + borderColor: 'transparent', + color: 'transparent', + }, + emphasis: { + itemStyle: { + borderColor: 'transparent', + color: 'transparent', + }, + }, + data: assistData, + }, + { + name: LEGEND.INCREASE, + type: 'bar', + stack: 'stack', + label: { + show: showValue, + position: 'top', + formatter, + }, + itemStyle: { + color: colorFn(LEGEND.INCREASE, sliceId), + }, + data: increaseData, + }, + { + name: LEGEND.DECREASE, + type: 'bar', + stack: 'stack', + label: { + show: showValue, + position: 'bottom', + formatter, + }, + itemStyle: { + color: colorFn(LEGEND.DECREASE, sliceId), + }, + data: decreaseData, + }, + { + name: LEGEND.TOTAL, + type: 'bar', + stack: 'stack', + label: { + show: showValue, + position: 'top', + formatter, + }, + itemStyle: { + color: colorFn(LEGEND.TOTAL, sliceId), + }, + data: totalData, + }, + ]; + + const echartOptions: EChartsOption = { + grid: { + ...defaultGrid, + top: theme.gridUnit * 7, + bottom: theme.gridUnit * 7, + left: theme.gridUnit * 5, + right: theme.gridUnit * 7, + }, + legend: { + show: showLegend, + data: [LEGEND.INCREASE, LEGEND.DECREASE, LEGEND.TOTAL], + }, + xAxis: { + type: 'category', + data: xAxisData, + name: xAxisLabel, + nameTextStyle: { + padding: [theme.gridUnit * 4, 0, 0, 0], + }, + nameLocation: 'middle', + axisLabel, + }, + yAxis: { + ...defaultYAxis, + type: 'value', + nameTextStyle: { + padding: [0, 0, theme.gridUnit * 5, 0], + }, + nameLocation: 'middle', + name: yAxisLabel, + axisLabel: { formatter: numberFormatter }, + }, + tooltip: { + ...getDefaultTooltip(refs), + appendToBody: true, + trigger: richTooltip ? 'axis' : 'item', + show: !inContextMenu, + formatter: (params: any) => + formatTooltip({ + theme, + params, + numberFormatter, + richTooltip, + }), + }, + series: barSeries, + }; + + return { + refs, + formData, + width, + height, + echartOptions, + setDataMask, + labelMap: Object.fromEntries(columnsLabelMap), + groupby, + selectedValues: filterState.selectedValues || [], + onContextMenu, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts new file mode 100644 index 0000000000000..9821cf3146392 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts @@ -0,0 +1,66 @@ +/** + * 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 { + ChartDataResponseResult, + ChartProps, + QueryFormData, + QueryFormMetric, +} from '@superset-ui/core'; +import { BarDataItemOption } from 'echarts/types/src/chart/bar/BarSeries'; +import { OptionDataValue } from 'echarts/types/src/util/types'; +import { + BaseTransformedProps, + CrossFilterTransformedProps, + LegendFormData, +} from '../types'; + +export type WaterfallFormXTicksLayout = + | '45°' + | '90°' + | 'auto' + | 'flat' + | 'staggered'; + +export type ISeriesData = + | BarDataItemOption + | OptionDataValue + | OptionDataValue[]; + +export type EchartsWaterfallFormData = QueryFormData & + LegendFormData & { + metric: QueryFormMetric; + yAxisLabel: string; + xAxisLabel: string; + yAxisFormat: string; + xTicksLayout?: WaterfallFormXTicksLayout; + series: string; + columns?: string; + }; + +export const DEFAULT_FORM_DATA: Partial = { + showLegend: true, +}; + +export interface EchartsWaterfallChartProps extends ChartProps { + formData: EchartsWaterfallFormData; + queriesData: ChartDataResponseResult[]; +} + +export type WaterfallChartTransformedProps = + BaseTransformedProps & CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index f8c7cf61036fb..1a7458a58605e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -35,6 +35,7 @@ export { default as EchartsTreemapChartPlugin } from './Treemap'; export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber'; export { default as EchartsSunburstChartPlugin } from './Sunburst'; export { default as EchartsBubbleChartPlugin } from './Bubble'; +export { default as EchartsWaterfallChartPlugin } from './Waterfall'; export { default as BoxPlotTransformProps } from './BoxPlot/transformProps'; export { default as FunnelTransformProps } from './Funnel/transformProps'; @@ -48,6 +49,7 @@ export { default as TreeTransformProps } from './Tree/transformProps'; export { default as TreemapTransformProps } from './Treemap/transformProps'; export { default as SunburstTransformProps } from './Sunburst/transformProps'; export { default as BubbleTransformProps } from './Bubble/transformProps'; +export { default as WaterfallTransformProps } from './Waterfall/transformProps'; export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts new file mode 100644 index 0000000000000..9c5d28376ba28 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts @@ -0,0 +1,38 @@ +/** + * 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 { SqlaFormData } from '@superset-ui/core'; +import buildQuery from '../../src/Waterfall/buildQuery'; + +describe('Waterfall buildQuery', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + metric: 'foo', + series: 'bar', + columns: 'baz', + viz_type: 'my_chart', + }; + + it('should build query fields from form data', () => { + const queryContext = buildQuery(formData as unknown as SqlaFormData); + const [query] = queryContext.queries; + expect(query.metrics).toEqual(['foo']); + expect(query.columns).toEqual(['bar', 'baz']); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts new file mode 100644 index 0000000000000..c221b9303358a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts @@ -0,0 +1,121 @@ +/** + * 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 { ChartProps, supersetTheme } from '@superset-ui/core'; +import { EchartsWaterfallChartProps } from '../../src/Waterfall/types'; +import transformProps from '../../src/Waterfall/transformProps'; + +describe('Waterfall tranformProps', () => { + const data = [ + { foo: 'Sylvester', bar: '2019', sum: 10 }, + { foo: 'Arnold', bar: '2019', sum: 3 }, + { foo: 'Sylvester', bar: '2020', sum: -10 }, + { foo: 'Arnold', bar: '2020', sum: 5 }, + ]; + + it('should tranform chart props for viz when breakdown not exist', () => { + const formData1 = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'sum', + series: 'bar', + }; + const chartProps = new ChartProps({ + formData: formData1, + width: 800, + height: 600, + queriesData: [ + { + data, + }, + ], + theme: supersetTheme, + }); + expect( + transformProps(chartProps as unknown as EchartsWaterfallChartProps), + ).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: [ + expect.objectContaining({ + data: [0, 8, '-'], + }), + expect.objectContaining({ + data: [13, '-', '-'], + }), + expect.objectContaining({ + data: ['-', 5, '-'], + }), + expect.objectContaining({ + data: ['-', '-', 8], + }), + ], + }), + }), + ); + }); + + it('should tranform chart props for viz when breakdown exist', () => { + const formData1 = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'sum', + series: 'bar', + columns: 'foo', + }; + const chartProps = new ChartProps({ + formData: formData1, + width: 800, + height: 600, + queriesData: [ + { + data, + }, + ], + theme: supersetTheme, + }); + expect( + transformProps(chartProps as unknown as EchartsWaterfallChartProps), + ).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: [ + expect.objectContaining({ + data: [0, 10, '-', 3, 3, '-'], + }), + expect.objectContaining({ + data: [10, 3, '-', '-', 5, '-'], + }), + expect.objectContaining({ + data: ['-', '-', '-', 10, '-', '-'], + }), + expect.objectContaining({ + data: ['-', '-', 13, '-', '-', 8], + }), + ], + }), + }), + ); + }); +}); diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index 451196c35dbac..f37a9155e1d71 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -66,6 +66,7 @@ import { EchartsTreeChartPlugin, EchartsSunburstChartPlugin, EchartsBubbleChartPlugin, + EchartsWaterfallChartPlugin, } from '@superset-ui/plugin-chart-echarts'; import { SelectFilterPlugin, @@ -153,6 +154,9 @@ export default class MainPreset extends Preset { new EchartsTimeseriesStepChartPlugin().configure({ key: 'echarts_timeseries_step', }), + new EchartsWaterfallChartPlugin().configure({ + key: 'waterfall', + }), new SelectFilterPlugin().configure({ key: 'filter_select' }), new RangeFilterPlugin().configure({ key: 'filter_range' }), new TimeFilterPlugin().configure({ key: 'filter_time' }),