feat(timeseries-chart): add percentage threshold input control (#17758)
* feat(timeseries-chart): add percentage threshold control for stack series labels * feat: move threshold vlues to an array * add tests for showValue, onlyTotal, and percentThreshold * feat: add another test * revert ChartProps typesetting, fix misnamed variable on form data type, and other minor changes * fix percentage threshold push equation * fix percentage threshold push equation in tests * change default on control to match form * attempt fix form defaults import Co-authored-by: Corbin Robb <corbin@Corbins-MacBook-Pro.local>
This commit is contained in:
parent
27000da2f8
commit
6bd4dd257a
|
|
@ -74,6 +74,9 @@ export const Timeseries = ({ width, height }) => {
|
|||
logAxis: boolean('Log axis', false),
|
||||
yAxisFormat: 'SMART_NUMBER',
|
||||
stack: boolean('Stack', false),
|
||||
showValue: boolean('Show Values', false),
|
||||
onlyTotal: boolean('Only Total', false),
|
||||
percentageThreshold: number('Percentage Threshold', 0),
|
||||
area: boolean('Area chart', false),
|
||||
markerEnabled: boolean('Enable markers', false),
|
||||
markerSize: number('Marker Size', 6),
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export default function transformProps(
|
|||
groupby,
|
||||
showValue,
|
||||
onlyTotal,
|
||||
percentageThreshold,
|
||||
xAxisTitle,
|
||||
yAxisTitle,
|
||||
xAxisTitleMargin,
|
||||
|
|
@ -130,6 +131,7 @@ export default function transformProps(
|
|||
|
||||
const totalStackedValues: number[] = [];
|
||||
const showValueIndexes: number[] = [];
|
||||
const thresholdValues: number[] = [];
|
||||
|
||||
rebasedData.forEach(data => {
|
||||
const values = Object.keys(data).reduce((prev, curr) => {
|
||||
|
|
@ -140,6 +142,7 @@ export default function transformProps(
|
|||
return prev + (value as number);
|
||||
}, 0);
|
||||
totalStackedValues.push(values);
|
||||
thresholdValues.push(((percentageThreshold || 0) / 100) * values);
|
||||
});
|
||||
|
||||
if (stack) {
|
||||
|
|
@ -168,6 +171,7 @@ export default function transformProps(
|
|||
onlyTotal,
|
||||
totalStackedValues,
|
||||
showValueIndexes,
|
||||
thresholdValues,
|
||||
richTooltip,
|
||||
});
|
||||
if (transformedSeries) series.push(transformedSeries);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export function transformSeries(
|
|||
formatter?: NumberFormatter;
|
||||
totalStackedValues?: number[];
|
||||
showValueIndexes?: number[];
|
||||
thresholdValues?: number[];
|
||||
richTooltip?: boolean;
|
||||
},
|
||||
): SeriesOption | undefined {
|
||||
|
|
@ -100,6 +101,7 @@ export function transformSeries(
|
|||
formatter,
|
||||
totalStackedValues = [],
|
||||
showValueIndexes = [],
|
||||
thresholdValues = [],
|
||||
richTooltip,
|
||||
} = opts;
|
||||
const contexts = seriesContexts[name || ''] || [];
|
||||
|
|
@ -211,8 +213,12 @@ export function transformSeries(
|
|||
} = params;
|
||||
const isSelectedLegend = currentSeries.legend === seriesName;
|
||||
if (!formatter) return numericValue;
|
||||
if (!stack || !onlyTotal || isSelectedLegend) {
|
||||
return formatter(numericValue);
|
||||
if (!stack || isSelectedLegend) return formatter(numericValue);
|
||||
if (!onlyTotal) {
|
||||
if (numericValue >= thresholdValues[dataIndex]) {
|
||||
return formatter(numericValue);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (seriesIndex === showValueIndexes[dataIndex]) {
|
||||
return formatter(totalStackedValues[dataIndex]);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
|||
groupby: QueryFormColumn[];
|
||||
showValue: boolean;
|
||||
onlyTotal: boolean;
|
||||
percentageThreshold: number;
|
||||
} & EchartsLegendFormData &
|
||||
EchartsTitleFormData;
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
|
|||
groupby: [],
|
||||
showValue: false,
|
||||
onlyTotal: false,
|
||||
percentageThreshold: 0,
|
||||
...DEFAULT_TITLE_FORM_DATA,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_LEGEND_FORM_DATA } from './types';
|
||||
import { DEFAULT_FORM_DATA } from './Timeseries/types';
|
||||
|
||||
const { legendMargin, legendOrientation, legendType, showLegend } =
|
||||
DEFAULT_LEGEND_FORM_DATA;
|
||||
|
|
@ -136,10 +137,29 @@ const onlyTotalControl = {
|
|||
},
|
||||
};
|
||||
|
||||
const percentageThresholdControl = {
|
||||
name: 'percentage_threshold',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Percentage threshold'),
|
||||
renderTrigger: true,
|
||||
isFloat: true,
|
||||
default: DEFAULT_FORM_DATA.percentageThreshold,
|
||||
description: t(
|
||||
'Minimum threshold in percentage points for showing labels.',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.show_value?.value) &&
|
||||
Boolean(controls?.stack?.value) &&
|
||||
Boolean(!controls?.only_total?.value),
|
||||
},
|
||||
};
|
||||
|
||||
export const showValueSection = [
|
||||
[showValueControl],
|
||||
[stackControl],
|
||||
[onlyTotalControl],
|
||||
[percentageThresholdControl],
|
||||
];
|
||||
|
||||
const richTooltipControl = {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,13 @@ import {
|
|||
FormulaAnnotationLayer,
|
||||
IntervalAnnotationLayer,
|
||||
TimeseriesAnnotationLayer,
|
||||
AnnotationStyle,
|
||||
AnnotationType,
|
||||
AnnotationSourceType,
|
||||
} from '@superset-ui/core';
|
||||
import transformProps from '../../src/Timeseries/transformProps';
|
||||
|
||||
describe('EchartsTimeseries tranformProps', () => {
|
||||
describe('EchartsTimeseries transformProps', () => {
|
||||
const formData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
|
|
@ -82,9 +85,10 @@ describe('EchartsTimeseries tranformProps', () => {
|
|||
it('should add a formula annotation to viz', () => {
|
||||
const formula: FormulaAnnotationLayer = {
|
||||
name: 'My Formula',
|
||||
annotationType: 'FORMULA',
|
||||
annotationType: AnnotationType.Formula,
|
||||
value: 'x+1',
|
||||
style: 'solid',
|
||||
style: AnnotationStyle.Solid,
|
||||
showLabel: true,
|
||||
show: true,
|
||||
};
|
||||
const chartProps = new ChartProps({
|
||||
|
|
@ -132,33 +136,36 @@ describe('EchartsTimeseries tranformProps', () => {
|
|||
|
||||
it('should add an interval, event and timeseries annotation to viz', () => {
|
||||
const event: EventAnnotationLayer = {
|
||||
annotationType: 'EVENT',
|
||||
annotationType: AnnotationType.Event,
|
||||
name: 'My Event',
|
||||
show: true,
|
||||
sourceType: 'NATIVE',
|
||||
style: 'solid',
|
||||
showLabel: true,
|
||||
sourceType: AnnotationSourceType.Native,
|
||||
style: AnnotationStyle.Solid,
|
||||
value: 1,
|
||||
};
|
||||
|
||||
const interval: IntervalAnnotationLayer = {
|
||||
annotationType: 'INTERVAL',
|
||||
annotationType: AnnotationType.Interval,
|
||||
name: 'My Interval',
|
||||
show: true,
|
||||
sourceType: 'table',
|
||||
showLabel: true,
|
||||
sourceType: AnnotationSourceType.Table,
|
||||
titleColumn: '',
|
||||
timeColumn: 'start',
|
||||
intervalEndColumn: '',
|
||||
descriptionColumns: [],
|
||||
style: 'dashed',
|
||||
style: AnnotationStyle.Dashed,
|
||||
value: 2,
|
||||
};
|
||||
|
||||
const timeseries: TimeseriesAnnotationLayer = {
|
||||
annotationType: 'TIME_SERIES',
|
||||
annotationType: AnnotationType.Timeseries,
|
||||
name: 'My Timeseries',
|
||||
show: true,
|
||||
sourceType: 'line',
|
||||
style: 'solid',
|
||||
showLabel: true,
|
||||
sourceType: AnnotationSourceType.Line,
|
||||
style: AnnotationStyle.Solid,
|
||||
titleColumn: '',
|
||||
value: 3,
|
||||
};
|
||||
|
|
@ -244,3 +251,198 @@ describe('EchartsTimeseries tranformProps', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Does transformProps transform series correctly', () => {
|
||||
type seriesDataType = [Date, number];
|
||||
type labelFormatterType = (params: {
|
||||
value: seriesDataType;
|
||||
dataIndex: number;
|
||||
seriesIndex: number;
|
||||
}) => string;
|
||||
type seriesType = {
|
||||
label: { show: boolean; formatter: labelFormatterType };
|
||||
data: seriesDataType[];
|
||||
name: string;
|
||||
};
|
||||
|
||||
const formData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'sum__num',
|
||||
groupby: ['foo', 'bar'],
|
||||
showValue: true,
|
||||
stack: true,
|
||||
onlyTotal: false,
|
||||
percentageThreshold: 50,
|
||||
};
|
||||
const queriesData = [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
'San Francisco': 1,
|
||||
'New York': 2,
|
||||
Boston: 1,
|
||||
__timestamp: 599616000000,
|
||||
},
|
||||
{
|
||||
'San Francisco': 3,
|
||||
'New York': 4,
|
||||
Boston: 1,
|
||||
__timestamp: 599916000000,
|
||||
},
|
||||
{
|
||||
'San Francisco': 5,
|
||||
'New York': 8,
|
||||
Boston: 6,
|
||||
__timestamp: 600216000000,
|
||||
},
|
||||
{
|
||||
'San Francisco': 2,
|
||||
'New York': 7,
|
||||
Boston: 2,
|
||||
__timestamp: 600516000000,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const chartPropsConfig = {
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData,
|
||||
};
|
||||
|
||||
const totalStackedValues = queriesData[0].data.reduce(
|
||||
(totals, currentStack) => {
|
||||
const total = Object.keys(currentStack).reduce((stackSum, key) => {
|
||||
if (key === '__timestamp') return stackSum;
|
||||
return stackSum + currentStack[key];
|
||||
}, 0);
|
||||
totals.push(total);
|
||||
return totals;
|
||||
},
|
||||
[] as number[],
|
||||
);
|
||||
|
||||
it('should show labels when showValue is true', () => {
|
||||
const chartProps = new ChartProps(chartPropsConfig);
|
||||
|
||||
const transformedSeries = transformProps(chartProps).echartOptions
|
||||
.series as seriesType[];
|
||||
|
||||
transformedSeries.forEach(series => {
|
||||
expect(series.label.show).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show labels when showValue is false', () => {
|
||||
const updatedChartPropsConfig = {
|
||||
...chartPropsConfig,
|
||||
formData: { ...formData, showValue: false },
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps(updatedChartPropsConfig);
|
||||
|
||||
const transformedSeries = transformProps(chartProps).echartOptions
|
||||
.series as seriesType[];
|
||||
|
||||
transformedSeries.forEach(series => {
|
||||
expect(series.label.show).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show only totals when onlyTotal is true', () => {
|
||||
const updatedChartPropsConfig = {
|
||||
...chartPropsConfig,
|
||||
formData: { ...formData, onlyTotal: true },
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps(updatedChartPropsConfig);
|
||||
|
||||
const transformedSeries = transformProps(chartProps).echartOptions
|
||||
.series as seriesType[];
|
||||
|
||||
const showValueIndexes: number[] = [];
|
||||
|
||||
transformedSeries.forEach((entry, seriesIndex) => {
|
||||
const { data = [] } = entry;
|
||||
(data as [Date, number][]).forEach((datum, dataIndex) => {
|
||||
if (datum[1] !== null) {
|
||||
showValueIndexes[dataIndex] = seriesIndex;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
transformedSeries.forEach((series, seriesIndex) => {
|
||||
expect(series.label.show).toBe(true);
|
||||
series.data.forEach((value, dataIndex) => {
|
||||
const params = {
|
||||
value,
|
||||
dataIndex,
|
||||
seriesIndex,
|
||||
};
|
||||
|
||||
let expectedLabel: string;
|
||||
|
||||
if (seriesIndex === showValueIndexes[dataIndex]) {
|
||||
expectedLabel = String(totalStackedValues[dataIndex]);
|
||||
} else {
|
||||
expectedLabel = '';
|
||||
}
|
||||
|
||||
expect(series.label.formatter(params)).toBe(expectedLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show labels on values >= percentageThreshold if onlyTotal is false', () => {
|
||||
const chartProps = new ChartProps(chartPropsConfig);
|
||||
|
||||
const transformedSeries = transformProps(chartProps).echartOptions
|
||||
.series as seriesType[];
|
||||
|
||||
const expectedThresholds = totalStackedValues.map(
|
||||
total => ((formData.percentageThreshold || 0) / 100) * total,
|
||||
);
|
||||
|
||||
transformedSeries.forEach((series, seriesIndex) => {
|
||||
expect(series.label.show).toBe(true);
|
||||
series.data.forEach((value, dataIndex) => {
|
||||
const params = {
|
||||
value,
|
||||
dataIndex,
|
||||
seriesIndex,
|
||||
};
|
||||
const expectedLabel =
|
||||
value[1] >= expectedThresholds[dataIndex] ? String(value[1]) : '';
|
||||
expect(series.label.formatter(params)).toBe(expectedLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply percentage threshold when showValue is true and stack is false', () => {
|
||||
const updatedChartPropsConfig = {
|
||||
...chartPropsConfig,
|
||||
formData: { ...formData, stack: false },
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps(updatedChartPropsConfig);
|
||||
|
||||
const transformedSeries = transformProps(chartProps).echartOptions
|
||||
.series as seriesType[];
|
||||
|
||||
transformedSeries.forEach((series, seriesIndex) => {
|
||||
expect(series.label.show).toBe(true);
|
||||
series.data.forEach((value, dataIndex) => {
|
||||
const params = {
|
||||
value,
|
||||
dataIndex,
|
||||
seriesIndex,
|
||||
};
|
||||
const expectedLabel = String(value[1]);
|
||||
expect(series.label.formatter(params)).toBe(expectedLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue