feat(plugin-chart-echarts): Echarts Waterfall (#17906)

Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
Stephen Liu
2023-10-06 21:08:16 +08:00
committed by GitHub
parent 0c40bea064
commit 17792a507c
14 changed files with 1124 additions and 0 deletions

View File

@@ -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 }) => (
<SuperChart
chartType="echarts-waterfall"
width={width}
height={height}
queriesData={[{ data }]}
formData={{
metric: `SUM(decomp_volume)`,
columns: 'due_to_group',
series: 'period',
x_ticks_layout: '45°',
adhocFilters: [
{
clause: 'WHERE',
comparator: '0',
expressionType: 'SIMPLE',
filterOptionName: 'filter_8ix98su8zu4_t4767ixmbp9',
isExtra: false,
isNew: false,
operator: '!=',
sqlExpression: null,
subject: 'period',
},
],
}}
/>
);

View File

@@ -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,
},
];

View File

@@ -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 (
<Echart
refs={refs}
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
/>
);
}

View File

@@ -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],
},
]);
}

View File

@@ -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 = '-';

View File

@@ -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',
),
},
},
],
[<div className="section-header">{t('X Axis')}</div>],
[
{
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'),
},
},
],
[<div className="section-header">{t('Y Axis')}</div>],
[
{
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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -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,
});
}
}

View File

@@ -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) =>
`
<div>${params.name}</div>
<div>
${params.marker}
<span style="
font-size:${theme.typography.sizes.m}px;
color:${theme.colors.grayscale.base};
font-weight:${theme.typography.weights.normal};
margin-left:${theme.gridUnit * 0.5}px;"
>
${params.seriesName}:
</span>
<span style="
float:right;
margin-left:${theme.gridUnit * 5}px;
font-size:${theme.typography.sizes.m}px;
color:${theme.colors.grayscale.base};
font-weight:${theme.typography.weights.bold}"
>
${numberFormatter(params.data)}
</span>
</div>
`;
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<string, DataRecord[]>());
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<string, string[]>();
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,
};
}

View File

@@ -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<EchartsWaterfallFormData> = {
showLegend: true,
};
export interface EchartsWaterfallChartProps extends ChartProps {
formData: EchartsWaterfallFormData;
queriesData: ChartDataResponseResult[];
}
export type WaterfallChartTransformedProps =
BaseTransformedProps<EchartsWaterfallFormData> & CrossFilterTransformedProps;

View File

@@ -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';

View File

@@ -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']);
});
});

View File

@@ -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],
}),
],
}),
}),
);
});
});

View File

@@ -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' }),