mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
562 lines
15 KiB
TypeScript
562 lines
15 KiB
TypeScript
/**
|
|
* 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 { DatasourceType, TimeGranularity, VizType } from '@superset-ui/core';
|
|
import { supersetTheme } from '@apache-superset/core/ui';
|
|
import transformProps from '../../src/BigNumber/BigNumberWithTrendline/transformProps';
|
|
import {
|
|
BigNumberDatum,
|
|
BigNumberWithTrendlineChartProps,
|
|
BigNumberWithTrendlineFormData,
|
|
} from '../../src/BigNumber/types';
|
|
import { TIMESERIES_CONSTANTS } from '../../src/constants';
|
|
|
|
const formData = {
|
|
metric: 'value',
|
|
colorPicker: {
|
|
r: 0,
|
|
g: 122,
|
|
b: 135,
|
|
a: 1,
|
|
},
|
|
compareLag: 1,
|
|
xAxis: '__timestamp',
|
|
timeGrainSqla: TimeGranularity.QUARTER,
|
|
granularitySqla: 'ds',
|
|
compareSuffix: 'over last quarter',
|
|
viz_type: VizType.BigNumber,
|
|
yAxisFormat: '.3s',
|
|
datasource: 'test_datasource',
|
|
};
|
|
|
|
const rawFormData: BigNumberWithTrendlineFormData = {
|
|
colorPicker: { b: 0, g: 0, r: 0 },
|
|
datasource: '1__table',
|
|
metric: 'value',
|
|
color_picker: {
|
|
r: 0,
|
|
g: 122,
|
|
b: 135,
|
|
a: 1,
|
|
},
|
|
compare_lag: 1,
|
|
x_axis: '__timestamp',
|
|
time_grain_sqla: TimeGranularity.QUARTER,
|
|
granularity_sqla: 'ds',
|
|
compare_suffix: 'over last quarter',
|
|
viz_type: VizType.BigNumber,
|
|
y_axis_format: '.3s',
|
|
xAxis: '__timestamp',
|
|
};
|
|
|
|
function generateProps(
|
|
data: BigNumberDatum[],
|
|
extraFormData = {},
|
|
extraQueryData: any = {},
|
|
): BigNumberWithTrendlineChartProps {
|
|
return {
|
|
width: 200,
|
|
height: 500,
|
|
annotationData: {},
|
|
datasource: {
|
|
id: 0,
|
|
name: '',
|
|
type: DatasourceType.Table,
|
|
columns: [],
|
|
metrics: [],
|
|
columnFormats: {},
|
|
verboseMap: {},
|
|
},
|
|
rawDatasource: {},
|
|
rawFormData,
|
|
hooks: {},
|
|
initialValues: {},
|
|
formData: {
|
|
...formData,
|
|
...extraFormData,
|
|
},
|
|
queriesData: [
|
|
{
|
|
data,
|
|
...extraQueryData,
|
|
},
|
|
],
|
|
ownState: {},
|
|
filterState: {},
|
|
behaviors: [],
|
|
theme: supersetTheme,
|
|
};
|
|
}
|
|
|
|
describe('BigNumberWithTrendline', () => {
|
|
const props = generateProps(
|
|
[
|
|
{
|
|
__timestamp: 0,
|
|
value: 1.2345,
|
|
},
|
|
{
|
|
__timestamp: 100,
|
|
value: null,
|
|
},
|
|
],
|
|
{ showTrendLine: true },
|
|
);
|
|
|
|
describe('transformProps()', () => {
|
|
test('should fallback and format time', () => {
|
|
const transformed = transformProps(props);
|
|
// the first item is the last item sorted by __timestamp
|
|
const lastDatum = transformed.trendLineData?.pop();
|
|
|
|
// should use last available value
|
|
expect(lastDatum?.[0]).toStrictEqual(100);
|
|
expect(lastDatum?.[1]).toBeNull();
|
|
|
|
// should get the last non-null value
|
|
expect(transformed.bigNumber).toStrictEqual(1.2345);
|
|
// bigNumberFallback is only set when bigNumber is null after aggregation
|
|
expect(transformed.bigNumberFallback).toBeNull();
|
|
|
|
// should successfully formatTime by granularity
|
|
// @ts-expect-error
|
|
expect(transformed.formatTime(new Date('2020-01-01'))).toStrictEqual(
|
|
'2020-01-01 00:00:00',
|
|
);
|
|
});
|
|
|
|
test('should respect datasource d3 format', () => {
|
|
const propsWithDatasource = {
|
|
...props,
|
|
datasource: {
|
|
...props.datasource,
|
|
metrics: [
|
|
{
|
|
label: 'value',
|
|
metric_name: 'value',
|
|
d3format: '.2f',
|
|
uuid: '1',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const transformed = transformProps(propsWithDatasource);
|
|
// @ts-expect-error
|
|
expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual(
|
|
'1.23',
|
|
);
|
|
});
|
|
|
|
test('should format with datasource currency', () => {
|
|
const propsWithDatasource = {
|
|
...props,
|
|
datasource: {
|
|
...props.datasource,
|
|
currencyFormats: {
|
|
value: { symbol: 'USD', symbolPosition: 'prefix' },
|
|
},
|
|
metrics: [
|
|
{
|
|
label: 'value',
|
|
metric_name: 'value',
|
|
d3format: '.2f',
|
|
currency: { symbol: 'USD', symbolPosition: 'prefix' },
|
|
uuid: '1',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const transformed = transformProps(propsWithDatasource);
|
|
// @ts-expect-error
|
|
expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual(
|
|
'$ 1.23',
|
|
);
|
|
});
|
|
|
|
test('should show X axis when showXAxis is true', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: true,
|
|
},
|
|
});
|
|
expect((transformed.echartOptions!.xAxis as { show: boolean }).show).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('should not show X axis when showXAxis is false', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: false,
|
|
},
|
|
});
|
|
expect((transformed.echartOptions!.xAxis as { show: boolean }).show).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('should show Y axis when showYAxis is true', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showYAxis: true,
|
|
},
|
|
});
|
|
expect((transformed.echartOptions!.yAxis as { show: boolean }).show).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('should not show Y axis when showYAxis is false', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showYAxis: false,
|
|
},
|
|
});
|
|
expect((transformed.echartOptions!.yAxis as { show: boolean }).show).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should respect min/max label visibility settings', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxisMinMaxLabels: false,
|
|
showYAxisMinMaxLabels: true,
|
|
},
|
|
});
|
|
const xAxis = transformed.echartOptions?.xAxis as any;
|
|
const yAxis = transformed.echartOptions?.yAxis as any;
|
|
|
|
expect(xAxis.axisLabel.showMinLabel).toBe(false);
|
|
expect(xAxis.axisLabel.showMaxLabel).toBe(false);
|
|
expect(yAxis.axisLabel.showMinLabel).toBe(true);
|
|
expect(yAxis.axisLabel.showMaxLabel).toBe(true);
|
|
});
|
|
|
|
test('should use minimal grid when both axes are hidden', () => {
|
|
const transformed = transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: false,
|
|
showYAxis: false,
|
|
},
|
|
});
|
|
|
|
expect(transformed.echartOptions?.grid).toEqual({
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
});
|
|
});
|
|
|
|
test('should use expanded grid when either axis is shown', () => {
|
|
const expandedGrid = {
|
|
containLabel: true,
|
|
bottom: TIMESERIES_CONSTANTS.gridOffsetBottom,
|
|
left: TIMESERIES_CONSTANTS.gridOffsetLeft,
|
|
right: TIMESERIES_CONSTANTS.gridOffsetRight,
|
|
top: TIMESERIES_CONSTANTS.gridOffsetTop,
|
|
};
|
|
|
|
expect(
|
|
transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: true,
|
|
showYAxis: false,
|
|
},
|
|
}).echartOptions?.grid,
|
|
).toEqual(expandedGrid);
|
|
expect(
|
|
transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: false,
|
|
showYAxis: true,
|
|
},
|
|
}).echartOptions?.grid,
|
|
).toEqual(expandedGrid);
|
|
expect(
|
|
transformProps({
|
|
...props,
|
|
formData: {
|
|
...props.formData,
|
|
showXAxis: true,
|
|
showYAxis: true,
|
|
},
|
|
}).echartOptions?.grid,
|
|
).toEqual(expandedGrid);
|
|
});
|
|
});
|
|
|
|
describe('BigNumberWithTrendline - Aggregation Tests', () => {
|
|
const baseProps = {
|
|
width: 800,
|
|
height: 600,
|
|
formData: {
|
|
colorPicker: { r: 0, g: 0, b: 0, a: 1 },
|
|
metric: 'metric',
|
|
aggregation: 'LAST_VALUE',
|
|
},
|
|
queriesData: [
|
|
{
|
|
data: [
|
|
{ __timestamp: 1607558400000, metric: 10 },
|
|
{ __timestamp: 1607558500000, metric: 30 },
|
|
{ __timestamp: 1607558600000, metric: 50 },
|
|
{ __timestamp: 1607558700000, metric: 60 },
|
|
],
|
|
colnames: ['__timestamp', 'metric'],
|
|
coltypes: ['TIMESTAMP', 'BIGINT'],
|
|
},
|
|
],
|
|
hooks: {},
|
|
filterState: {},
|
|
datasource: {
|
|
columnFormats: {},
|
|
currencyFormats: {},
|
|
},
|
|
rawDatasource: {},
|
|
rawFormData: {},
|
|
theme: {
|
|
colors: {
|
|
grayscale: {
|
|
light5: '#fafafa',
|
|
},
|
|
},
|
|
},
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const propsWithEvenData = {
|
|
...baseProps,
|
|
queriesData: [
|
|
{
|
|
data: [
|
|
{ __timestamp: 1607558400000, metric: 10 },
|
|
{ __timestamp: 1607558500000, metric: 20 },
|
|
{ __timestamp: 1607558600000, metric: 30 },
|
|
{ __timestamp: 1607558700000, metric: 40 },
|
|
],
|
|
colnames: ['__timestamp', 'metric'],
|
|
coltypes: ['TIMESTAMP', 'BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
test('should correctly calculate SUM', () => {
|
|
const props = {
|
|
...baseProps,
|
|
formData: { ...baseProps.formData, aggregation: 'sum' },
|
|
queriesData: [
|
|
baseProps.queriesData[0],
|
|
{
|
|
data: [{ metric: 150 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(150);
|
|
});
|
|
|
|
test('should correctly calculate AVG', () => {
|
|
const props = {
|
|
...baseProps,
|
|
formData: { ...baseProps.formData, aggregation: 'mean' },
|
|
queriesData: [
|
|
baseProps.queriesData[0],
|
|
{
|
|
data: [{ metric: 37.5 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(37.5);
|
|
});
|
|
|
|
test('should correctly calculate MIN', () => {
|
|
const props = {
|
|
...baseProps,
|
|
formData: { ...baseProps.formData, aggregation: 'min' },
|
|
queriesData: [
|
|
baseProps.queriesData[0],
|
|
{
|
|
data: [{ metric: 10 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(10);
|
|
});
|
|
|
|
test('should correctly calculate MAX', () => {
|
|
const props = {
|
|
...baseProps,
|
|
formData: { ...baseProps.formData, aggregation: 'max' },
|
|
queriesData: [
|
|
baseProps.queriesData[0],
|
|
{
|
|
data: [{ metric: 60 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(60);
|
|
});
|
|
|
|
test('should correctly calculate MEDIAN (odd count)', () => {
|
|
const oddCountProps = {
|
|
...baseProps,
|
|
queriesData: [
|
|
{
|
|
data: [
|
|
{ __timestamp: 1607558300000, metric: 10 },
|
|
{ __timestamp: 1607558400000, metric: 20 },
|
|
{ __timestamp: 1607558500000, metric: 30 },
|
|
{ __timestamp: 1607558600000, metric: 40 },
|
|
{ __timestamp: 1607558700000, metric: 50 },
|
|
],
|
|
colnames: ['__timestamp', 'metric'],
|
|
coltypes: ['TIMESTAMP', 'BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const props = {
|
|
...oddCountProps,
|
|
formData: { ...oddCountProps.formData, aggregation: 'median' },
|
|
queriesData: [
|
|
oddCountProps.queriesData[0],
|
|
{
|
|
data: [{ metric: 30 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(30);
|
|
});
|
|
|
|
test('should correctly calculate MEDIAN (even count)', () => {
|
|
const props = {
|
|
...propsWithEvenData,
|
|
formData: { ...propsWithEvenData.formData, aggregation: 'median' },
|
|
queriesData: [
|
|
propsWithEvenData.queriesData[0],
|
|
{
|
|
data: [{ metric: 25 }],
|
|
colnames: ['metric'],
|
|
coltypes: ['BIGINT'],
|
|
},
|
|
],
|
|
} as unknown as BigNumberWithTrendlineChartProps;
|
|
|
|
const transformed = transformProps(props);
|
|
expect(transformed.bigNumber).toStrictEqual(25);
|
|
});
|
|
|
|
test('should return the LAST_VALUE correctly', () => {
|
|
const transformed = transformProps(baseProps);
|
|
expect(transformed.bigNumber).toStrictEqual(10);
|
|
});
|
|
});
|
|
|
|
test('BigNumberWithTrendline AUTO mode should detect single currency', () => {
|
|
const props = generateProps(
|
|
[
|
|
{ __timestamp: 1607558400000, value: 1000, currency_code: 'USD' },
|
|
{ __timestamp: 1607558500000, value: 2000, currency_code: 'USD' },
|
|
],
|
|
{
|
|
yAxisFormat: ',.2f',
|
|
currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' },
|
|
},
|
|
);
|
|
props.datasource.currencyCodeColumn = 'currency_code';
|
|
|
|
const transformed = transformProps(props);
|
|
// The headerFormatter should include $ for USD
|
|
expect(transformed.headerFormatter(1000)).toContain('$');
|
|
});
|
|
|
|
test('BigNumberWithTrendline AUTO mode should use neutral formatting for mixed currencies', () => {
|
|
const props = generateProps(
|
|
[
|
|
{ __timestamp: 1607558400000, value: 1000, currency_code: 'USD' },
|
|
{ __timestamp: 1607558500000, value: 2000, currency_code: 'EUR' },
|
|
],
|
|
{
|
|
yAxisFormat: ',.2f',
|
|
currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' },
|
|
},
|
|
);
|
|
props.datasource.currencyCodeColumn = 'currency_code';
|
|
|
|
const transformed = transformProps(props);
|
|
// With mixed currencies, should not show currency symbol
|
|
const formatted = transformed.headerFormatter(1000);
|
|
expect(formatted).not.toContain('$');
|
|
expect(formatted).not.toContain('€');
|
|
});
|
|
|
|
test('BigNumberWithTrendline should preserve static currency format', () => {
|
|
const props = generateProps(
|
|
[
|
|
{ __timestamp: 1607558400000, value: 1000, currency_code: 'USD' },
|
|
{ __timestamp: 1607558500000, value: 2000, currency_code: 'EUR' },
|
|
],
|
|
{
|
|
yAxisFormat: ',.2f',
|
|
currencyFormat: { symbol: 'GBP', symbolPosition: 'prefix' },
|
|
},
|
|
);
|
|
props.datasource.currencyCodeColumn = 'currency_code';
|
|
|
|
const transformed = transformProps(props);
|
|
// Static mode should always show £
|
|
expect(transformed.headerFormatter(1000)).toContain('£');
|
|
});
|