From eab0009101a295acf4d8d31df8a57f8fe0deb517 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Thu, 9 Jun 2022 00:59:10 +0800 Subject: [PATCH] feat(plugin-chart-echarts): [feature-parity] support extra control for the area chart V2 (#16493) * feat(echarts): [feature-parity] support extra control * add extra control for plugin * refactor: extract ExtraControl * fix: lint * fix some problems --- .../components/RadioButtonControl.tsx | 2 +- .../src/MixedTimeseries/transformProps.ts | 4 +- .../src/MixedTimeseries/types.ts | 5 +- .../src/Timeseries/Area/controlPanel.tsx | 36 +++++- .../src/Timeseries/EchartsTimeseries.tsx | 26 ++-- .../src/Timeseries/transformProps.ts | 66 +++++------ .../src/Timeseries/transformers.ts | 13 +- .../src/Timeseries/types.ts | 4 +- .../src/components/ExtraControls.tsx | 112 ++++++++++++++++++ .../plugin-chart-echarts/src/constants.ts | 18 ++- .../plugin-chart-echarts/src/controls.tsx | 6 +- .../plugins/plugin-chart-echarts/src/types.ts | 5 + .../plugin-chart-echarts/src/utils/series.ts | 83 +++++++++++-- .../src/components/Chart/ChartRenderer.jsx | 1 + .../components/gridComponents/Chart.jsx | 3 + .../components/gridComponents/ChartHolder.jsx | 15 ++- .../src/dashboard/containers/Chart.jsx | 4 +- .../charts/getFormDataWithExtraFilters.ts | 8 +- .../util/getFormDataWithExtraFilters.test.ts | 8 ++ 19 files changed, 349 insertions(+), 70 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx index 497e331133..285b92e66e 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx @@ -31,7 +31,7 @@ export interface RadioButtonControlProps { description?: string; options: RadioButtonOption[]; hovered?: boolean; - value?: string; + value?: JsonValue; onChange: (opt: RadioButtonOption[0]) => void; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 139dcd9af7..62ed57268f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -190,7 +190,7 @@ export default function transformProps( areaOpacity: opacity, seriesType, showValue, - stack, + stack: Boolean(stack), yAxisIndex, filterState, seriesKey: entry.name, @@ -207,7 +207,7 @@ export default function transformProps( areaOpacity: opacityB, seriesType: seriesTypeB, showValue: showValueB, - stack: stackB, + stack: Boolean(stackB), yAxisIndex: yAxisIndexB, filterState, seriesKey: primarySeries.has(entry.name as string) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index b5f37551e1..51938436fb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -32,6 +32,7 @@ import { EchartsLegendFormData, EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA, + StackType, } from '../types'; import { DEFAULT_FORM_DATA as TIMESERIES_DEFAULTS, @@ -78,8 +79,8 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { seriesTypeB: EchartsTimeseriesSeriesType; showValue: boolean; showValueB: boolean; - stack: boolean; - stackB: boolean; + stack: StackType; + stackB: StackType; yAxisIndex?: number; yAxisIndexB?: number; groupby: QueryFormColumn[]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index b973cb6782..e43dda8903 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -34,10 +34,12 @@ import { } from '../types'; import { legendSection, + onlyTotalControl, + showValueControl, richTooltipSection, - showValueSection, xAxisControl, } from '../../controls'; +import { AreaChartExtraControlsOptions } from '../../constants'; const { contributionMode, @@ -132,7 +134,37 @@ const config: ControlPanelConfig = { }, }, ], - ...showValueSection, + [showValueControl], + [ + { + name: 'stack', + config: { + type: 'SelectControl', + label: t('Stacked Style'), + renderTrigger: true, + choices: AreaChartExtraControlsOptions, + default: null, + description: t('Stack series on top of each other'), + }, + }, + ], + [onlyTotalControl], + [ + { + name: 'show_extra_controls', + config: { + type: 'CheckboxControl', + label: t('Extra Controls'), + renderTrigger: true, + default: false, + description: t( + 'Whether to show extra controls or not. Extra controls ' + + 'include things like making mulitBar charts stacked ' + + 'or side by side.', + ), + }, + }, + ], [ { name: 'markerEnabled', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 2bf103e2bd..a9947e0d55 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -24,8 +24,10 @@ import { EchartsHandler, EventHandlers } from '../types'; import Echart from '../components/Echart'; import { TimeseriesChartTransformedProps } from './types'; import { currentSeries } from '../utils/series'; +import { ExtraControls } from '../components/ExtraControls'; const TIMER_DURATION = 300; + // @ts-ignore export default function EchartsTimeseries({ formData, @@ -36,6 +38,7 @@ export default function EchartsTimeseries({ labelMap, selectedValues, setDataMask, + setControlValue, legendData = [], }: TimeseriesChartTransformedProps) { const { emitFilter, stack } = formData; @@ -120,7 +123,7 @@ export default function EchartsTimeseries({ }, }); }, - [groupby, labelMap, setDataMask], + [groupby, labelMap, setDataMask, emitFilter], ); const eventHandlers: EventHandlers = { @@ -195,14 +198,17 @@ export default function EchartsTimeseries({ }; return ( - + <> + + + ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index db142c8aa2..89d5c1e03b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -30,6 +30,7 @@ import { isIntervalAnnotationLayer, isTimeseriesAnnotationLayer, TimeseriesChartDataResponseResult, + t, } from '@superset-ui/core'; import { isDerivedSeries } from '@superset-ui/chart-controls'; import { EChartsCoreOption, SeriesOption } from 'echarts'; @@ -51,6 +52,8 @@ import { getAxisType, getColtypesMapping, getLegendProps, + extractDataTotalValues, + extractShowValueIndexes, } from '../utils/series'; import { extractAnnotationLabels } from '../utils/annotation'; import { @@ -72,7 +75,11 @@ import { transformSeries, transformTimeseriesAnnotation, } from './transformers'; -import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants'; +import { + AreaChartExtraControlsValue, + TIMESERIES_CONSTANTS, + TIMEGRAIN_TO_TIMESTAMP, +} from '../constants'; export default function transformProps( chartProps: EchartsTimeseriesChartProps, @@ -140,46 +147,35 @@ export default function transformProps( const xAxisCol = verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS); const isHorizontal = orientation === OrientationType.horizontal; + const { totalStackedValues, thresholdValues } = extractDataTotalValues( + rebasedData, + { + stack, + percentageThreshold, + xAxisCol, + }, + ); const rawSeries = extractSeries(rebasedData, { fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, xAxis: xAxisCol, removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter, + stack, + totalStackedValues, isHorizontal, }); + const showValueIndexes = extractShowValueIndexes(rawSeries, { + stack, + }); const seriesContexts = extractForecastSeriesContexts( Object.values(rawSeries).map(series => series.name as string), ); + const isAreaExpand = stack === AreaChartExtraControlsValue.Expand; const xAxisDataType = dataTypes?.[xAxisCol]; const xAxisType = getAxisType(xAxisDataType); const series: SeriesOption[] = []; - const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat); - - const totalStackedValues: number[] = []; - const showValueIndexes: number[] = []; - const thresholdValues: number[] = []; - - rebasedData.forEach(data => { - const values = Object.keys(data).reduce((prev, curr) => { - if (curr === xAxisCol) { - return prev; - } - const value = data[curr] || 0; - return prev + (value as number); - }, 0); - totalStackedValues.push(values); - thresholdValues.push(((percentageThreshold || 0) / 100) * values); - }); - - if (stack) { - rawSeries.forEach((entry, seriesIndex) => { - const { data = [] } = entry; - (data as [Date, number][]).forEach((datum, dataIndex) => { - if (datum[1] !== null) { - showValueIndexes[dataIndex] = seriesIndex; - } - }); - }); - } + const formatter = getNumberFormatter( + contributionMode || isAreaExpand ? ',.0%' : yAxisFormat, + ); rawSeries.forEach(entry => { const lineStyle = isDerivedSeries(entry, chartProps.rawFormData) @@ -266,7 +262,7 @@ export default function transformProps( let [min, max] = (yAxisBounds || []).map(parseYAxisBound); // default to 0-100% range when doing row-level contribution chart - if (contributionMode === 'row' && stack) { + if ((contributionMode === 'row' || isAreaExpand) && stack) { if (min === undefined) min = 0; if (max === undefined) max = 1; } @@ -291,7 +287,10 @@ export default function transformProps( {}, ); - const { setDataMask = () => {} } = hooks; + const { + setDataMask = () => {}, + setControlValue = (...args: unknown[]) => {}, + } = hooks; const addYAxisLabelOffset = !!yAxisTitle; const addXAxisLabelOffset = !!xAxisTitle; @@ -406,8 +405,8 @@ export default function transformProps( dataZoom: { yAxisIndex: false, title: { - zoom: 'zoom area', - back: 'restore zoom', + zoom: t('zoom area'), + back: t('restore zoom'), }, }, }, @@ -433,6 +432,7 @@ export default function transformProps( labelMap, selectedValues, setDataMask, + setControlValue, width, legendData, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index 4ab7309dbc..93565de46c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -52,7 +52,7 @@ import { import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { extractForecastSeriesContext } from '../utils/forecast'; -import { ForecastSeriesEnum, LegendOrientation } from '../types'; +import { ForecastSeriesEnum, LegendOrientation, StackType } from '../types'; import { EchartsTimeseriesSeriesType } from './types'; import { @@ -62,7 +62,11 @@ import { parseAnnotationOpacity, } from '../utils/annotation'; import { currentSeries, getChartPadding } from '../utils/series'; -import { OpacityEnum, TIMESERIES_CONSTANTS } from '../constants'; +import { + AreaChartExtraControlsValue, + OpacityEnum, + TIMESERIES_CONSTANTS, +} from '../constants'; export function transformSeries( series: SeriesOption, @@ -75,7 +79,7 @@ export function transformSeries( markerSize?: number; areaOpacity?: number; seriesType?: EchartsTimeseriesSeriesType; - stack?: boolean; + stack?: StackType; yAxisIndex?: number; showValue?: boolean; onlyTotal?: boolean; @@ -225,6 +229,7 @@ export function transformSeries( const { value, dataIndex, seriesIndex, seriesName } = params; const numericValue = isHorizontal ? value[0] : value[1]; const isSelectedLegend = currentSeries.legend === seriesName; + const isAreaExpand = stack === AreaChartExtraControlsValue.Expand; if (!formatter) return numericValue; if (!stack || isSelectedLegend) return formatter(numericValue); if (!onlyTotal) { @@ -234,7 +239,7 @@ export function transformSeries( return ''; } if (seriesIndex === showValueIndexes[dataIndex]) { - return formatter(totalStackedValues[dataIndex]); + return formatter(isAreaExpand ? 1 : totalStackedValues[dataIndex]); } return ''; }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 0d2499ccfc..d9b7708146 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -31,6 +31,7 @@ import { EChartTransformedProps, EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA, + StackType, } from '../types'; export enum EchartsTimeseriesContributionType { @@ -72,7 +73,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { orderDesc: boolean; rowLimit: number; seriesType: EchartsTimeseriesSeriesType; - stack: boolean; + stack: StackType; tooltipTimeFormat?: string; truncateYAxis: boolean; yAxisFormat?: string; @@ -86,6 +87,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { groupby: QueryFormColumn[]; showValue: boolean; onlyTotal: boolean; + showExtraControls: boolean; percentageThreshold: number; orientation?: OrientationType; } & EchartsLegendFormData & diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx new file mode 100644 index 0000000000..10217b3add --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx @@ -0,0 +1,112 @@ +/** + * 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, { useState, useEffect, useMemo, useCallback } from 'react'; +import { HandlerFunction, JsonValue, styled } from '@superset-ui/core'; +import { + RadioButtonOption, + sharedControlComponents, +} from '@superset-ui/chart-controls'; +import { AreaChartExtraControlsOptions } from '../constants'; + +const { RadioButtonControl } = sharedControlComponents; + +const ExtraControlsWrapper = styled.div` + text-align: center; +`; + +export function useExtraControl< + F extends { + stack: any; + area: boolean; + }, +>({ + formData, + setControlValue, +}: { + formData: F; + setControlValue?: HandlerFunction; +}) { + const { stack, area } = formData; + const [extraValue, setExtraValue] = useState( + stack ?? undefined, + ); + + useEffect(() => { + setExtraValue(stack); + }, [stack]); + + const extraControlsOptions = useMemo(() => { + if (area) { + return AreaChartExtraControlsOptions; + } + return []; + }, [area]); + + const extraControlsHandler = useCallback( + (value: RadioButtonOption[0]) => { + if (area) { + if (setControlValue) { + setControlValue('stack', value); + setExtraValue(value); + } + } + }, + [area, setControlValue], + ); + + return { + extraControlsOptions, + extraControlsHandler, + extraValue, + }; +} + +export function ExtraControls< + F extends { + stack: any; + area: boolean; + showExtraControls: boolean; + }, +>({ + formData, + setControlValue, +}: { + formData: F; + setControlValue?: HandlerFunction; +}) { + const { extraControlsOptions, extraControlsHandler, extraValue } = + useExtraControl({ + formData, + setControlValue, + }); + + if (!formData.showExtraControls) { + return null; + } + + return ( + + + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index deef2f2e8c..513a0bebc1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -17,7 +17,8 @@ * under the License. */ -import { TimeGranularity } from '@superset-ui/core'; +import { JsonValue, t, TimeGranularity } from '@superset-ui/core'; +import { ReactNode } from 'react'; import { LabelPositionEnum } from './types'; // eslint-disable-next-line import/prefer-default-export @@ -37,6 +38,7 @@ export const TIMESERIES_CONSTANTS = { dataZoomStart: 0, dataZoomEnd: 100, yAxisLabelTopOffset: 20, + extraControlsOffset: 22, }; export const LABEL_POSITION: [LabelPositionEnum, string][] = [ @@ -61,6 +63,20 @@ export enum OpacityEnum { NonTransparent = 1, } +export enum AreaChartExtraControlsValue { + Stack = 'Stack', + Expand = 'Expand', +} + +export const AreaChartExtraControlsOptions: [ + JsonValue, + Exclude, +][] = [ + [null, t('None')], + [AreaChartExtraControlsValue.Stack, t('Stack')], + [AreaChartExtraControlsValue.Expand, t('Expand')], +]; + export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.HOUR]: 3600 * 1000, [TimeGranularity.DAY]: 3600 * 1000 * 24, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index eca4723887..b8d54fc09a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -108,7 +108,7 @@ export const legendSection: ControlSetRow[] = [ [legendMarginControl], ]; -const showValueControl: ControlSetItem = { +export const showValueControl: ControlSetItem = { name: 'show_value', config: { type: 'CheckboxControl', @@ -119,7 +119,7 @@ const showValueControl: ControlSetItem = { }, }; -const stackControl: ControlSetItem = { +export const stackControl: ControlSetItem = { name: 'stack', config: { type: 'CheckboxControl', @@ -130,7 +130,7 @@ const stackControl: ControlSetItem = { }, }; -const onlyTotalControl: ControlSetItem = { +export const onlyTotalControl: ControlSetItem = { name: 'only_total', config: { type: 'CheckboxControl', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index f50397c9ef..d84b7079c4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -18,12 +18,14 @@ */ import { DataRecordValue, + HandlerFunction, QueryFormColumn, SetDataMaskHook, } from '@superset-ui/core'; import { EChartsCoreOption, ECharts } from 'echarts'; import { TooltipMarker } from 'echarts/types/src/util/format'; import { OptionName } from 'echarts/types/src/util/types'; +import { AreaChartExtraControlsValue } from './constants'; export type EchartsStylesProps = { height: number; @@ -115,6 +117,7 @@ export interface EChartTransformedProps { echartOptions: EChartsCoreOption; emitFilter: boolean; setDataMask: SetDataMaskHook; + setControlValue?: HandlerFunction; labelMap: Record; groupby: QueryFormColumn[]; selectedValues: Record; @@ -137,4 +140,6 @@ export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { yAxisTitlePosition: 'Top', }; +export type StackType = boolean | null | Partial; + export * from './Timeseries/types'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index fa8a23138c..23710cd6d1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -28,20 +28,79 @@ import { TimeFormatter, } from '@superset-ui/core'; import { format, LegendComponentOption, SeriesOption } from 'echarts'; -import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants'; -import { LegendOrientation, LegendType } from '../types'; +import { + AreaChartExtraControlsValue, + NULL_STRING, + TIMESERIES_CONSTANTS, +} from '../constants'; +import { LegendOrientation, LegendType, StackType } from '../types'; import { defaultLegendPadding } from '../defaults'; function isDefined(value: T | undefined | null): boolean { return value !== undefined && value !== null; } +export function extractDataTotalValues( + data: DataRecord[], + opts: { + stack: StackType; + percentageThreshold: number; + xAxisCol: string; + }, +): { + totalStackedValues: number[]; + thresholdValues: number[]; +} { + const totalStackedValues: number[] = []; + const thresholdValues: number[] = []; + const { stack, percentageThreshold, xAxisCol } = opts; + if (stack) { + data.forEach(datum => { + const values = Object.keys(datum).reduce((prev, curr) => { + if (curr === xAxisCol) { + return prev; + } + const value = datum[curr] || 0; + return prev + (value as number); + }, 0); + totalStackedValues.push(values); + thresholdValues.push(((percentageThreshold || 0) / 100) * values); + }); + } + return { + totalStackedValues, + thresholdValues, + }; +} + +export function extractShowValueIndexes( + series: SeriesOption[], + opts: { + stack: StackType; + }, +): number[] { + const showValueIndexes: number[] = []; + if (opts.stack) { + series.forEach((entry, seriesIndex) => { + const { data = [] } = entry; + (data as [any, number][]).forEach((datum, dataIndex) => { + if (datum[1] !== null) { + showValueIndexes[dataIndex] = seriesIndex; + } + }); + }); + } + return showValueIndexes; +} + export function extractSeries( data: DataRecord[], opts: { fillNeighborValue?: number; xAxis?: string; removeNulls?: boolean; + stack?: StackType; + totalStackedValues?: number[]; isHorizontal?: boolean; } = {}, ): SeriesOption[] { @@ -49,6 +108,8 @@ export function extractSeries( fillNeighborValue, xAxis = DTTM_ALIAS, removeNulls = false, + stack = false, + totalStackedValues = [], isHorizontal = false, } = opts; if (data.length === 0) return []; @@ -66,14 +127,20 @@ export function extractSeries( .map((row, idx) => { const isNextToDefinedValue = isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]); - return [ - row[xAxis], + const isFillNeighborValue = !isDefined(row[key]) && isNextToDefinedValue && - fillNeighborValue !== undefined - ? fillNeighborValue - : row[key], - ]; + fillNeighborValue !== undefined; + let value: DataRecordValue | undefined = row[key]; + if (isFillNeighborValue) { + value = fillNeighborValue; + } else if ( + stack === AreaChartExtraControlsValue.Expand && + totalStackedValues.length > 0 + ) { + value = ((value || 0) as number) / totalStackedValues[idx]; + } + return [row[xAxis], value]; }) .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null)) .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)), diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index 45feb6ffd5..ed330ab7af 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -113,6 +113,7 @@ class ChartRenderer extends React.Component { nextProps.labelColors !== this.props.labelColors || nextProps.sharedLabelColors !== this.props.sharedLabelColors || nextProps.formData.color_scheme !== this.props.formData.color_scheme || + nextProps.formData.stack !== this.props.formData.stack || nextProps.cacheBusterProp !== this.props.cacheBusterProp ); } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index e5d19e931c..5232f51e4d 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -51,6 +51,7 @@ const propTypes = { updateSliceName: PropTypes.func.isRequired, isComponentVisible: PropTypes.bool, handleToggleFullSize: PropTypes.func.isRequired, + setControlValue: PropTypes.func, // from redux chart: chartPropShape.isRequired, @@ -348,6 +349,7 @@ export default class Chart extends React.Component { filterState, handleToggleFullSize, isFullSize, + setControlValue, filterboxMigrationState, postTransformProps, datasetsStatus, @@ -475,6 +477,7 @@ export default class Chart extends React.Component { timeout={timeout} triggerQuery={chart.triggerQuery} vizType={slice.viz_type} + setControlValue={setControlValue} isDeactivatedViz={isDeactivatedViz} filterboxMigrationState={filterboxMigrationState} postTransformProps={postTransformProps} diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index eeac856615..2363b1610e 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -191,12 +191,14 @@ class ChartHolder extends React.Component { outlinedComponentId: null, outlinedColumnName: null, directPathLastUpdated: 0, + extraControls: {}, }; this.handleChangeFocus = this.handleChangeFocus.bind(this); this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this); + this.handleExtraControl = this.handleExtraControl.bind(this); this.handlePostTransformProps = this.handlePostTransformProps.bind(this); } @@ -252,13 +254,22 @@ class ChartHolder extends React.Component { setFullSizeChartId(isFullSize ? null : chartId); } + handleExtraControl(name, value) { + this.setState(prevState => ({ + extraControls: { + ...prevState.extraControls, + [name]: value, + }, + })); + } + handlePostTransformProps(props) { this.props.postAddSliceFromDashboard(); return props; } render() { - const { isFocused } = this.state; + const { isFocused, extraControls } = this.state; const { component, parentComponent, @@ -374,6 +385,8 @@ class ChartHolder extends React.Component { isComponentVisible={isComponentVisible} handleToggleFullSize={this.handleToggleFullSize} isFullSize={isFullSize} + setControlValue={this.handleExtraControl} + extraControls={extraControls} postTransformProps={this.handlePostTransformProps} /> {editMode && ( diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 79b4e936da..81d06b8566 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -55,7 +55,7 @@ function mapStateToProps( }, ownProps, ) { - const { id } = ownProps; + const { id, extraControls, setControlValue } = ownProps; const chart = chartQueries[id] || EMPTY_OBJECT; const datasource = (chart && chart.form_data && datasources[chart.form_data.datasource]) || @@ -76,6 +76,7 @@ function mapStateToProps( sliceId: id, nativeFilters, dataMask, + extraControls, labelColors, sharedLabelColors, }); @@ -100,6 +101,7 @@ function mapStateToProps( ownState: dataMask[id]?.ownState, filterState: dataMask[id]?.filterState, maxRows: common.conf.SQL_MAX_ROW, + setControlValue, filterboxMigrationState: dashboardState.filterboxMigrationState, datasetsStatus, }; diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 54e0417b27..0bbabfcde5 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -45,6 +45,7 @@ export interface GetFormDataWithExtraFiltersArguments { sliceId: number; dataMask: DataMaskStateWithId; nativeFilters: NativeFiltersState; + extraControls: Record; labelColors?: Record; sharedLabelColors?: Record; } @@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({ sliceId, layout, dataMask, + extraControls, labelColors, sharedLabelColors, }: GetFormDataWithExtraFiltersArguments) { @@ -85,6 +87,9 @@ export default function getFormDataWithExtraFilters({ !!cachedFormData && areObjectsEqual(cachedFormData?.dataMask, dataMask, { ignoreUndefined: true, + }) && + areObjectsEqual(cachedFormData?.extraControls, extraControls, { + ignoreUndefined: true, }) ) { return cachedFormData; @@ -117,10 +122,11 @@ export default function getFormDataWithExtraFilters({ ...(colorScheme && { color_scheme: colorScheme }), extra_filters: getEffectiveExtraFilters(filters), ...extraData, + ...extraControls, }; cachedFiltersByChart[sliceId] = filters; - cachedFormdataByChart[sliceId] = { ...formData, dataMask }; + cachedFormdataByChart[sliceId] = { ...formData, dataMask, extraControls }; return formData; } diff --git a/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts b/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts index fda5edc1a9..021a488e37 100644 --- a/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts +++ b/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts @@ -71,6 +71,9 @@ describe('getFormDataWithExtraFilters', () => { }, }, layout: {}, + extraControls: { + stack: 'Stacked', + }, }; it('should include filters from the passed filters', () => { @@ -87,4 +90,9 @@ describe('getFormDataWithExtraFilters', () => { val: ['pink', 'purple'], }); }); + + it('should compose extra control', () => { + const result = getFormDataWithExtraFilters(mockArgs); + expect(result.stack).toEqual('Stacked'); + }); });