Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(timeseries-chart): add percentage threshold input control #17758

Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default function transformProps(
groupby,
showValue,
onlyTotal,
percentageThreshold,
xAxisTitle,
yAxisTitle,
xAxisTitleMargin,
Expand All @@ -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) => {
Expand All @@ -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) {
Expand Down Expand Up @@ -168,6 +171,7 @@ export default function transformProps(
onlyTotal,
totalStackedValues,
showValueIndexes,
thresholdValues,
richTooltip,
});
if (transformedSeries) series.push(transformedSeries);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function transformSeries(
formatter?: NumberFormatter;
totalStackedValues?: number[];
showValueIndexes?: number[];
thresholdValues?: number[];
richTooltip?: boolean;
},
): SeriesOption | undefined {
Expand All @@ -100,6 +101,7 @@ export function transformSeries(
formatter,
totalStackedValues = [],
showValueIndexes = [],
thresholdValues = [],
richTooltip,
} = opts;
const contexts = seriesContexts[name || ''] || [];
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
groupby: QueryFormColumn[];
showValue: boolean;
onlyTotal: boolean;
percentageThreshold: number;
} & EchartsLegendFormData &
EchartsTitleFormData;

Expand Down Expand Up @@ -117,6 +118,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
groupby: [],
showValue: false,
onlyTotal: false,
percentageThreshold: 0,
...DEFAULT_TITLE_FORM_DATA,
};

Expand Down
20 changes: 20 additions & 0 deletions superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 = [
corbinrobb marked this conversation as resolved.
Show resolved Hide resolved
{
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);
});
});
});
});