mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
feat(plugin-chart-echarts): Echarts Waterfall (#17906)
Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
This commit is contained in:
@@ -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',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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],
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -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 = '-';
|
||||
@@ -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 |
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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' }),
|
||||
|
||||
Reference in New Issue
Block a user