Skip to content

Commit

Permalink
feat(timeseries-chart): add percentage threshold input control (#17758)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
corbinrobb and Corbin Robb authored Jan 12, 2022
1 parent 27000da commit 6bd4dd2
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 14 deletions.
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 = [
{
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);
});
});
});
});

0 comments on commit 6bd4dd2

Please sign in to comment.