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 0000000000000..91ef20f515f92
Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/images/thumbnail.png differ
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' }),