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
This commit is contained in:
Stephen Liu 2022-06-09 00:59:10 +08:00 committed by GitHub
parent 0238492df7
commit eab0009101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 349 additions and 70 deletions

View File

@ -31,7 +31,7 @@ export interface RadioButtonControlProps {
description?: string; description?: string;
options: RadioButtonOption[]; options: RadioButtonOption[];
hovered?: boolean; hovered?: boolean;
value?: string; value?: JsonValue;
onChange: (opt: RadioButtonOption[0]) => void; onChange: (opt: RadioButtonOption[0]) => void;
} }

View File

@ -190,7 +190,7 @@ export default function transformProps(
areaOpacity: opacity, areaOpacity: opacity,
seriesType, seriesType,
showValue, showValue,
stack, stack: Boolean(stack),
yAxisIndex, yAxisIndex,
filterState, filterState,
seriesKey: entry.name, seriesKey: entry.name,
@ -207,7 +207,7 @@ export default function transformProps(
areaOpacity: opacityB, areaOpacity: opacityB,
seriesType: seriesTypeB, seriesType: seriesTypeB,
showValue: showValueB, showValue: showValueB,
stack: stackB, stack: Boolean(stackB),
yAxisIndex: yAxisIndexB, yAxisIndex: yAxisIndexB,
filterState, filterState,
seriesKey: primarySeries.has(entry.name as string) seriesKey: primarySeries.has(entry.name as string)

View File

@ -32,6 +32,7 @@ import {
EchartsLegendFormData, EchartsLegendFormData,
EchartsTitleFormData, EchartsTitleFormData,
DEFAULT_TITLE_FORM_DATA, DEFAULT_TITLE_FORM_DATA,
StackType,
} from '../types'; } from '../types';
import { import {
DEFAULT_FORM_DATA as TIMESERIES_DEFAULTS, DEFAULT_FORM_DATA as TIMESERIES_DEFAULTS,
@ -78,8 +79,8 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
seriesTypeB: EchartsTimeseriesSeriesType; seriesTypeB: EchartsTimeseriesSeriesType;
showValue: boolean; showValue: boolean;
showValueB: boolean; showValueB: boolean;
stack: boolean; stack: StackType;
stackB: boolean; stackB: StackType;
yAxisIndex?: number; yAxisIndex?: number;
yAxisIndexB?: number; yAxisIndexB?: number;
groupby: QueryFormColumn[]; groupby: QueryFormColumn[];

View File

@ -34,10 +34,12 @@ import {
} from '../types'; } from '../types';
import { import {
legendSection, legendSection,
onlyTotalControl,
showValueControl,
richTooltipSection, richTooltipSection,
showValueSection,
xAxisControl, xAxisControl,
} from '../../controls'; } from '../../controls';
import { AreaChartExtraControlsOptions } from '../../constants';
const { const {
contributionMode, 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', name: 'markerEnabled',

View File

@ -24,8 +24,10 @@ import { EchartsHandler, EventHandlers } from '../types';
import Echart from '../components/Echart'; import Echart from '../components/Echart';
import { TimeseriesChartTransformedProps } from './types'; import { TimeseriesChartTransformedProps } from './types';
import { currentSeries } from '../utils/series'; import { currentSeries } from '../utils/series';
import { ExtraControls } from '../components/ExtraControls';
const TIMER_DURATION = 300; const TIMER_DURATION = 300;
// @ts-ignore // @ts-ignore
export default function EchartsTimeseries({ export default function EchartsTimeseries({
formData, formData,
@ -36,6 +38,7 @@ export default function EchartsTimeseries({
labelMap, labelMap,
selectedValues, selectedValues,
setDataMask, setDataMask,
setControlValue,
legendData = [], legendData = [],
}: TimeseriesChartTransformedProps) { }: TimeseriesChartTransformedProps) {
const { emitFilter, stack } = formData; const { emitFilter, stack } = formData;
@ -120,7 +123,7 @@ export default function EchartsTimeseries({
}, },
}); });
}, },
[groupby, labelMap, setDataMask], [groupby, labelMap, setDataMask, emitFilter],
); );
const eventHandlers: EventHandlers = { const eventHandlers: EventHandlers = {
@ -195,14 +198,17 @@ export default function EchartsTimeseries({
}; };
return ( return (
<Echart <>
ref={echartRef} <ExtraControls formData={formData} setControlValue={setControlValue} />
height={height} <Echart
width={width} ref={echartRef}
echartOptions={echartOptions} height={height}
eventHandlers={eventHandlers} width={width}
zrEventHandlers={zrEventHandlers} echartOptions={echartOptions}
selectedValues={selectedValues} eventHandlers={eventHandlers}
/> zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
/>
</>
); );
} }

View File

@ -30,6 +30,7 @@ import {
isIntervalAnnotationLayer, isIntervalAnnotationLayer,
isTimeseriesAnnotationLayer, isTimeseriesAnnotationLayer,
TimeseriesChartDataResponseResult, TimeseriesChartDataResponseResult,
t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { isDerivedSeries } from '@superset-ui/chart-controls'; import { isDerivedSeries } from '@superset-ui/chart-controls';
import { EChartsCoreOption, SeriesOption } from 'echarts'; import { EChartsCoreOption, SeriesOption } from 'echarts';
@ -51,6 +52,8 @@ import {
getAxisType, getAxisType,
getColtypesMapping, getColtypesMapping,
getLegendProps, getLegendProps,
extractDataTotalValues,
extractShowValueIndexes,
} from '../utils/series'; } from '../utils/series';
import { extractAnnotationLabels } from '../utils/annotation'; import { extractAnnotationLabels } from '../utils/annotation';
import { import {
@ -72,7 +75,11 @@ import {
transformSeries, transformSeries,
transformTimeseriesAnnotation, transformTimeseriesAnnotation,
} from './transformers'; } from './transformers';
import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants'; import {
AreaChartExtraControlsValue,
TIMESERIES_CONSTANTS,
TIMEGRAIN_TO_TIMESTAMP,
} from '../constants';
export default function transformProps( export default function transformProps(
chartProps: EchartsTimeseriesChartProps, chartProps: EchartsTimeseriesChartProps,
@ -140,46 +147,35 @@ export default function transformProps(
const xAxisCol = const xAxisCol =
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS); verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
const isHorizontal = orientation === OrientationType.horizontal; const isHorizontal = orientation === OrientationType.horizontal;
const { totalStackedValues, thresholdValues } = extractDataTotalValues(
rebasedData,
{
stack,
percentageThreshold,
xAxisCol,
},
);
const rawSeries = extractSeries(rebasedData, { const rawSeries = extractSeries(rebasedData, {
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
xAxis: xAxisCol, xAxis: xAxisCol,
removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter, removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
stack,
totalStackedValues,
isHorizontal, isHorizontal,
}); });
const showValueIndexes = extractShowValueIndexes(rawSeries, {
stack,
});
const seriesContexts = extractForecastSeriesContexts( const seriesContexts = extractForecastSeriesContexts(
Object.values(rawSeries).map(series => series.name as string), Object.values(rawSeries).map(series => series.name as string),
); );
const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
const xAxisDataType = dataTypes?.[xAxisCol]; const xAxisDataType = dataTypes?.[xAxisCol];
const xAxisType = getAxisType(xAxisDataType); const xAxisType = getAxisType(xAxisDataType);
const series: SeriesOption[] = []; const series: SeriesOption[] = [];
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat); const formatter = getNumberFormatter(
contributionMode || isAreaExpand ? ',.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;
}
});
});
}
rawSeries.forEach(entry => { rawSeries.forEach(entry => {
const lineStyle = isDerivedSeries(entry, chartProps.rawFormData) const lineStyle = isDerivedSeries(entry, chartProps.rawFormData)
@ -266,7 +262,7 @@ export default function transformProps(
let [min, max] = (yAxisBounds || []).map(parseYAxisBound); let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
// default to 0-100% range when doing row-level contribution chart // 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 (min === undefined) min = 0;
if (max === undefined) max = 1; 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 addYAxisLabelOffset = !!yAxisTitle;
const addXAxisLabelOffset = !!xAxisTitle; const addXAxisLabelOffset = !!xAxisTitle;
@ -406,8 +405,8 @@ export default function transformProps(
dataZoom: { dataZoom: {
yAxisIndex: false, yAxisIndex: false,
title: { title: {
zoom: 'zoom area', zoom: t('zoom area'),
back: 'restore zoom', back: t('restore zoom'),
}, },
}, },
}, },
@ -433,6 +432,7 @@ export default function transformProps(
labelMap, labelMap,
selectedValues, selectedValues,
setDataMask, setDataMask,
setControlValue,
width, width,
legendData, legendData,
}; };

View File

@ -52,7 +52,7 @@ import {
import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { extractForecastSeriesContext } from '../utils/forecast'; import { extractForecastSeriesContext } from '../utils/forecast';
import { ForecastSeriesEnum, LegendOrientation } from '../types'; import { ForecastSeriesEnum, LegendOrientation, StackType } from '../types';
import { EchartsTimeseriesSeriesType } from './types'; import { EchartsTimeseriesSeriesType } from './types';
import { import {
@ -62,7 +62,11 @@ import {
parseAnnotationOpacity, parseAnnotationOpacity,
} from '../utils/annotation'; } from '../utils/annotation';
import { currentSeries, getChartPadding } from '../utils/series'; import { currentSeries, getChartPadding } from '../utils/series';
import { OpacityEnum, TIMESERIES_CONSTANTS } from '../constants'; import {
AreaChartExtraControlsValue,
OpacityEnum,
TIMESERIES_CONSTANTS,
} from '../constants';
export function transformSeries( export function transformSeries(
series: SeriesOption, series: SeriesOption,
@ -75,7 +79,7 @@ export function transformSeries(
markerSize?: number; markerSize?: number;
areaOpacity?: number; areaOpacity?: number;
seriesType?: EchartsTimeseriesSeriesType; seriesType?: EchartsTimeseriesSeriesType;
stack?: boolean; stack?: StackType;
yAxisIndex?: number; yAxisIndex?: number;
showValue?: boolean; showValue?: boolean;
onlyTotal?: boolean; onlyTotal?: boolean;
@ -225,6 +229,7 @@ export function transformSeries(
const { value, dataIndex, seriesIndex, seriesName } = params; const { value, dataIndex, seriesIndex, seriesName } = params;
const numericValue = isHorizontal ? value[0] : value[1]; const numericValue = isHorizontal ? value[0] : value[1];
const isSelectedLegend = currentSeries.legend === seriesName; const isSelectedLegend = currentSeries.legend === seriesName;
const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
if (!formatter) return numericValue; if (!formatter) return numericValue;
if (!stack || isSelectedLegend) return formatter(numericValue); if (!stack || isSelectedLegend) return formatter(numericValue);
if (!onlyTotal) { if (!onlyTotal) {
@ -234,7 +239,7 @@ export function transformSeries(
return ''; return '';
} }
if (seriesIndex === showValueIndexes[dataIndex]) { if (seriesIndex === showValueIndexes[dataIndex]) {
return formatter(totalStackedValues[dataIndex]); return formatter(isAreaExpand ? 1 : totalStackedValues[dataIndex]);
} }
return ''; return '';
}, },

View File

@ -31,6 +31,7 @@ import {
EChartTransformedProps, EChartTransformedProps,
EchartsTitleFormData, EchartsTitleFormData,
DEFAULT_TITLE_FORM_DATA, DEFAULT_TITLE_FORM_DATA,
StackType,
} from '../types'; } from '../types';
export enum EchartsTimeseriesContributionType { export enum EchartsTimeseriesContributionType {
@ -72,7 +73,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
orderDesc: boolean; orderDesc: boolean;
rowLimit: number; rowLimit: number;
seriesType: EchartsTimeseriesSeriesType; seriesType: EchartsTimeseriesSeriesType;
stack: boolean; stack: StackType;
tooltipTimeFormat?: string; tooltipTimeFormat?: string;
truncateYAxis: boolean; truncateYAxis: boolean;
yAxisFormat?: string; yAxisFormat?: string;
@ -86,6 +87,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
groupby: QueryFormColumn[]; groupby: QueryFormColumn[];
showValue: boolean; showValue: boolean;
onlyTotal: boolean; onlyTotal: boolean;
showExtraControls: boolean;
percentageThreshold: number; percentageThreshold: number;
orientation?: OrientationType; orientation?: OrientationType;
} & EchartsLegendFormData & } & EchartsLegendFormData &

View File

@ -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<JsonValue | undefined>(
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<F>({
formData,
setControlValue,
});
if (!formData.showExtraControls) {
return null;
}
return (
<ExtraControlsWrapper>
<RadioButtonControl
options={extraControlsOptions}
onChange={extraControlsHandler}
value={extraValue}
/>
</ExtraControlsWrapper>
);
}

View File

@ -17,7 +17,8 @@
* under the License. * 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'; import { LabelPositionEnum } from './types';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
@ -37,6 +38,7 @@ export const TIMESERIES_CONSTANTS = {
dataZoomStart: 0, dataZoomStart: 0,
dataZoomEnd: 100, dataZoomEnd: 100,
yAxisLabelTopOffset: 20, yAxisLabelTopOffset: 20,
extraControlsOffset: 22,
}; };
export const LABEL_POSITION: [LabelPositionEnum, string][] = [ export const LABEL_POSITION: [LabelPositionEnum, string][] = [
@ -61,6 +63,20 @@ export enum OpacityEnum {
NonTransparent = 1, NonTransparent = 1,
} }
export enum AreaChartExtraControlsValue {
Stack = 'Stack',
Expand = 'Expand',
}
export const AreaChartExtraControlsOptions: [
JsonValue,
Exclude<ReactNode, null | undefined | boolean>,
][] = [
[null, t('None')],
[AreaChartExtraControlsValue.Stack, t('Stack')],
[AreaChartExtraControlsValue.Expand, t('Expand')],
];
export const TIMEGRAIN_TO_TIMESTAMP = { export const TIMEGRAIN_TO_TIMESTAMP = {
[TimeGranularity.HOUR]: 3600 * 1000, [TimeGranularity.HOUR]: 3600 * 1000,
[TimeGranularity.DAY]: 3600 * 1000 * 24, [TimeGranularity.DAY]: 3600 * 1000 * 24,

View File

@ -108,7 +108,7 @@ export const legendSection: ControlSetRow[] = [
[legendMarginControl], [legendMarginControl],
]; ];
const showValueControl: ControlSetItem = { export const showValueControl: ControlSetItem = {
name: 'show_value', name: 'show_value',
config: { config: {
type: 'CheckboxControl', type: 'CheckboxControl',
@ -119,7 +119,7 @@ const showValueControl: ControlSetItem = {
}, },
}; };
const stackControl: ControlSetItem = { export const stackControl: ControlSetItem = {
name: 'stack', name: 'stack',
config: { config: {
type: 'CheckboxControl', type: 'CheckboxControl',
@ -130,7 +130,7 @@ const stackControl: ControlSetItem = {
}, },
}; };
const onlyTotalControl: ControlSetItem = { export const onlyTotalControl: ControlSetItem = {
name: 'only_total', name: 'only_total',
config: { config: {
type: 'CheckboxControl', type: 'CheckboxControl',

View File

@ -18,12 +18,14 @@
*/ */
import { import {
DataRecordValue, DataRecordValue,
HandlerFunction,
QueryFormColumn, QueryFormColumn,
SetDataMaskHook, SetDataMaskHook,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { EChartsCoreOption, ECharts } from 'echarts'; import { EChartsCoreOption, ECharts } from 'echarts';
import { TooltipMarker } from 'echarts/types/src/util/format'; import { TooltipMarker } from 'echarts/types/src/util/format';
import { OptionName } from 'echarts/types/src/util/types'; import { OptionName } from 'echarts/types/src/util/types';
import { AreaChartExtraControlsValue } from './constants';
export type EchartsStylesProps = { export type EchartsStylesProps = {
height: number; height: number;
@ -115,6 +117,7 @@ export interface EChartTransformedProps<F> {
echartOptions: EChartsCoreOption; echartOptions: EChartsCoreOption;
emitFilter: boolean; emitFilter: boolean;
setDataMask: SetDataMaskHook; setDataMask: SetDataMaskHook;
setControlValue?: HandlerFunction;
labelMap: Record<string, DataRecordValue[]>; labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[]; groupby: QueryFormColumn[];
selectedValues: Record<number, string>; selectedValues: Record<number, string>;
@ -137,4 +140,6 @@ export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = {
yAxisTitlePosition: 'Top', yAxisTitlePosition: 'Top',
}; };
export type StackType = boolean | null | Partial<AreaChartExtraControlsValue>;
export * from './Timeseries/types'; export * from './Timeseries/types';

View File

@ -28,20 +28,79 @@ import {
TimeFormatter, TimeFormatter,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { format, LegendComponentOption, SeriesOption } from 'echarts'; import { format, LegendComponentOption, SeriesOption } from 'echarts';
import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants'; import {
import { LegendOrientation, LegendType } from '../types'; AreaChartExtraControlsValue,
NULL_STRING,
TIMESERIES_CONSTANTS,
} from '../constants';
import { LegendOrientation, LegendType, StackType } from '../types';
import { defaultLegendPadding } from '../defaults'; import { defaultLegendPadding } from '../defaults';
function isDefined<T>(value: T | undefined | null): boolean { function isDefined<T>(value: T | undefined | null): boolean {
return value !== undefined && value !== null; 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( export function extractSeries(
data: DataRecord[], data: DataRecord[],
opts: { opts: {
fillNeighborValue?: number; fillNeighborValue?: number;
xAxis?: string; xAxis?: string;
removeNulls?: boolean; removeNulls?: boolean;
stack?: StackType;
totalStackedValues?: number[];
isHorizontal?: boolean; isHorizontal?: boolean;
} = {}, } = {},
): SeriesOption[] { ): SeriesOption[] {
@ -49,6 +108,8 @@ export function extractSeries(
fillNeighborValue, fillNeighborValue,
xAxis = DTTM_ALIAS, xAxis = DTTM_ALIAS,
removeNulls = false, removeNulls = false,
stack = false,
totalStackedValues = [],
isHorizontal = false, isHorizontal = false,
} = opts; } = opts;
if (data.length === 0) return []; if (data.length === 0) return [];
@ -66,14 +127,20 @@ export function extractSeries(
.map((row, idx) => { .map((row, idx) => {
const isNextToDefinedValue = const isNextToDefinedValue =
isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]); isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]);
return [ const isFillNeighborValue =
row[xAxis],
!isDefined(row[key]) && !isDefined(row[key]) &&
isNextToDefinedValue && isNextToDefinedValue &&
fillNeighborValue !== undefined fillNeighborValue !== undefined;
? fillNeighborValue let value: DataRecordValue | undefined = row[key];
: 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)) .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
.map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)), .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),

View File

@ -113,6 +113,7 @@ class ChartRenderer extends React.Component {
nextProps.labelColors !== this.props.labelColors || nextProps.labelColors !== this.props.labelColors ||
nextProps.sharedLabelColors !== this.props.sharedLabelColors || nextProps.sharedLabelColors !== this.props.sharedLabelColors ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme || nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.formData.stack !== this.props.formData.stack ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp nextProps.cacheBusterProp !== this.props.cacheBusterProp
); );
} }

View File

@ -51,6 +51,7 @@ const propTypes = {
updateSliceName: PropTypes.func.isRequired, updateSliceName: PropTypes.func.isRequired,
isComponentVisible: PropTypes.bool, isComponentVisible: PropTypes.bool,
handleToggleFullSize: PropTypes.func.isRequired, handleToggleFullSize: PropTypes.func.isRequired,
setControlValue: PropTypes.func,
// from redux // from redux
chart: chartPropShape.isRequired, chart: chartPropShape.isRequired,
@ -348,6 +349,7 @@ export default class Chart extends React.Component {
filterState, filterState,
handleToggleFullSize, handleToggleFullSize,
isFullSize, isFullSize,
setControlValue,
filterboxMigrationState, filterboxMigrationState,
postTransformProps, postTransformProps,
datasetsStatus, datasetsStatus,
@ -475,6 +477,7 @@ export default class Chart extends React.Component {
timeout={timeout} timeout={timeout}
triggerQuery={chart.triggerQuery} triggerQuery={chart.triggerQuery}
vizType={slice.viz_type} vizType={slice.viz_type}
setControlValue={setControlValue}
isDeactivatedViz={isDeactivatedViz} isDeactivatedViz={isDeactivatedViz}
filterboxMigrationState={filterboxMigrationState} filterboxMigrationState={filterboxMigrationState}
postTransformProps={postTransformProps} postTransformProps={postTransformProps}

View File

@ -191,12 +191,14 @@ class ChartHolder extends React.Component {
outlinedComponentId: null, outlinedComponentId: null,
outlinedColumnName: null, outlinedColumnName: null,
directPathLastUpdated: 0, directPathLastUpdated: 0,
extraControls: {},
}; };
this.handleChangeFocus = this.handleChangeFocus.bind(this); this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this);
this.handleToggleFullSize = this.handleToggleFullSize.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this);
this.handleExtraControl = this.handleExtraControl.bind(this);
this.handlePostTransformProps = this.handlePostTransformProps.bind(this); this.handlePostTransformProps = this.handlePostTransformProps.bind(this);
} }
@ -252,13 +254,22 @@ class ChartHolder extends React.Component {
setFullSizeChartId(isFullSize ? null : chartId); setFullSizeChartId(isFullSize ? null : chartId);
} }
handleExtraControl(name, value) {
this.setState(prevState => ({
extraControls: {
...prevState.extraControls,
[name]: value,
},
}));
}
handlePostTransformProps(props) { handlePostTransformProps(props) {
this.props.postAddSliceFromDashboard(); this.props.postAddSliceFromDashboard();
return props; return props;
} }
render() { render() {
const { isFocused } = this.state; const { isFocused, extraControls } = this.state;
const { const {
component, component,
parentComponent, parentComponent,
@ -374,6 +385,8 @@ class ChartHolder extends React.Component {
isComponentVisible={isComponentVisible} isComponentVisible={isComponentVisible}
handleToggleFullSize={this.handleToggleFullSize} handleToggleFullSize={this.handleToggleFullSize}
isFullSize={isFullSize} isFullSize={isFullSize}
setControlValue={this.handleExtraControl}
extraControls={extraControls}
postTransformProps={this.handlePostTransformProps} postTransformProps={this.handlePostTransformProps}
/> />
{editMode && ( {editMode && (

View File

@ -55,7 +55,7 @@ function mapStateToProps(
}, },
ownProps, ownProps,
) { ) {
const { id } = ownProps; const { id, extraControls, setControlValue } = ownProps;
const chart = chartQueries[id] || EMPTY_OBJECT; const chart = chartQueries[id] || EMPTY_OBJECT;
const datasource = const datasource =
(chart && chart.form_data && datasources[chart.form_data.datasource]) || (chart && chart.form_data && datasources[chart.form_data.datasource]) ||
@ -76,6 +76,7 @@ function mapStateToProps(
sliceId: id, sliceId: id,
nativeFilters, nativeFilters,
dataMask, dataMask,
extraControls,
labelColors, labelColors,
sharedLabelColors, sharedLabelColors,
}); });
@ -100,6 +101,7 @@ function mapStateToProps(
ownState: dataMask[id]?.ownState, ownState: dataMask[id]?.ownState,
filterState: dataMask[id]?.filterState, filterState: dataMask[id]?.filterState,
maxRows: common.conf.SQL_MAX_ROW, maxRows: common.conf.SQL_MAX_ROW,
setControlValue,
filterboxMigrationState: dashboardState.filterboxMigrationState, filterboxMigrationState: dashboardState.filterboxMigrationState,
datasetsStatus, datasetsStatus,
}; };

View File

@ -45,6 +45,7 @@ export interface GetFormDataWithExtraFiltersArguments {
sliceId: number; sliceId: number;
dataMask: DataMaskStateWithId; dataMask: DataMaskStateWithId;
nativeFilters: NativeFiltersState; nativeFilters: NativeFiltersState;
extraControls: Record<string, string | boolean | null>;
labelColors?: Record<string, string>; labelColors?: Record<string, string>;
sharedLabelColors?: Record<string, string>; sharedLabelColors?: Record<string, string>;
} }
@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({
sliceId, sliceId,
layout, layout,
dataMask, dataMask,
extraControls,
labelColors, labelColors,
sharedLabelColors, sharedLabelColors,
}: GetFormDataWithExtraFiltersArguments) { }: GetFormDataWithExtraFiltersArguments) {
@ -85,6 +87,9 @@ export default function getFormDataWithExtraFilters({
!!cachedFormData && !!cachedFormData &&
areObjectsEqual(cachedFormData?.dataMask, dataMask, { areObjectsEqual(cachedFormData?.dataMask, dataMask, {
ignoreUndefined: true, ignoreUndefined: true,
}) &&
areObjectsEqual(cachedFormData?.extraControls, extraControls, {
ignoreUndefined: true,
}) })
) { ) {
return cachedFormData; return cachedFormData;
@ -117,10 +122,11 @@ export default function getFormDataWithExtraFilters({
...(colorScheme && { color_scheme: colorScheme }), ...(colorScheme && { color_scheme: colorScheme }),
extra_filters: getEffectiveExtraFilters(filters), extra_filters: getEffectiveExtraFilters(filters),
...extraData, ...extraData,
...extraControls,
}; };
cachedFiltersByChart[sliceId] = filters; cachedFiltersByChart[sliceId] = filters;
cachedFormdataByChart[sliceId] = { ...formData, dataMask }; cachedFormdataByChart[sliceId] = { ...formData, dataMask, extraControls };
return formData; return formData;
} }

View File

@ -71,6 +71,9 @@ describe('getFormDataWithExtraFilters', () => {
}, },
}, },
layout: {}, layout: {},
extraControls: {
stack: 'Stacked',
},
}; };
it('should include filters from the passed filters', () => { it('should include filters from the passed filters', () => {
@ -87,4 +90,9 @@ describe('getFormDataWithExtraFilters', () => {
val: ['pink', 'purple'], val: ['pink', 'purple'],
}); });
}); });
it('should compose extra control', () => {
const result = getFormDataWithExtraFilters(mockArgs);
expect(result.stack).toEqual('Stacked');
});
}); });