refactor: improve code smell for postprocessing (#1368)

* refactor: improve code smell for postprocessing

* jest UT

* best practice for jest
This commit is contained in:
Yongjie Zhao 2021-09-20 14:55:08 +01:00
parent eed58d6658
commit 0f4a06d15e
15 changed files with 656 additions and 358 deletions

View File

@ -0,0 +1,63 @@
/**
* 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 limitationsxw
* under the License.
*/
import {
PostProcessingBoxplot,
getMetricLabel,
ensureIsArray,
QueryFormColumn,
} from '@superset-ui/core';
import { PostProcessingFactory } from './types';
type BoxPlotQueryObjectWhiskerType = PostProcessingBoxplot['options']['whisker_type'];
const PERCENTILE_REGEX = /(\d+)\/(\d+) percentiles/;
export const boxplotOperator: PostProcessingFactory<PostProcessingBoxplot | undefined> = (
formData,
queryObject,
) => {
const { whiskerOptions } = formData;
if (whiskerOptions) {
let whiskerType: BoxPlotQueryObjectWhiskerType;
let percentiles: [number, number] | undefined;
const percentileMatch = PERCENTILE_REGEX.exec(whiskerOptions as string);
if (whiskerOptions === 'Tukey' || !whiskerOptions) {
whiskerType = 'tukey';
} else if (whiskerOptions === 'Min/max (no outliers)') {
whiskerType = 'min/max';
} else if (percentileMatch) {
whiskerType = 'percentile';
percentiles = [parseInt(percentileMatch[1], 10), parseInt(percentileMatch[2], 10)];
} else {
throw new Error(`Unsupported whisker type: ${whiskerOptions}`);
}
return {
operation: 'boxplot',
options: {
whisker_type: whiskerType,
percentiles,
groupby: ensureIsArray(queryObject.groupby as QueryFormColumn[]),
metrics: ensureIsArray(queryObject.metrics).map(getMetricLabel),
},
};
}
return undefined;
};

View File

@ -0,0 +1,35 @@
/**
* 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 limitationsxw
* under the License.
*/
import { PostProcessingContribution } from '@superset-ui/core';
import { PostProcessingFactory } from './types';
export const contributionOperator: PostProcessingFactory<PostProcessingContribution | undefined> = (
formData,
queryObject,
) => {
if (formData.contributionMode) {
return {
operation: 'contribution',
options: {
orientation: formData.contributionMode,
},
};
}
return undefined;
};

View File

@ -23,4 +23,7 @@ export { timeComparePivotOperator } from './timeComparePivotOperator';
export { sortOperator } from './sortOperator';
export { pivotOperator } from './pivotOperator';
export { resampleOperator } from './resampleOperator';
export { contributionOperator } from './contributionOperator';
export { prophetOperator } from './prophetOperator';
export { boxplotOperator } from './boxplotOperator';
export * from './utils';

View File

@ -0,0 +1,40 @@
/**
* 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 limitationsxw
* under the License.
*/
import { PostProcessingProphet } from '@superset-ui/core';
import { PostProcessingFactory } from './types';
export const prophetOperator: PostProcessingFactory<PostProcessingProphet | undefined> = (
formData,
queryObject,
) => {
if (formData.forecastEnabled) {
return {
operation: 'prophet',
options: {
time_grain: formData.time_grain_sqla,
periods: parseInt(formData.forecastPeriods, 10),
confidence_interval: parseFloat(formData.forecastInterval),
yearly_seasonality: formData.forecastSeasonalityYearly,
weekly_seasonality: formData.forecastSeasonalityWeekly,
daily_seasonality: formData.forecastSeasonalityDaily,
},
};
}
return undefined;
};

View File

@ -0,0 +1,109 @@
/**
* 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 { QueryObject, SqlaFormData } from '@superset-ui/core';
import { boxplotOperator } from '../../../src';
const formData: SqlaFormData = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
time_grain_sqla: 'P1Y',
datasource: 'foo',
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
granularity: 'P1Y',
};
test('should skip boxplotOperator', () => {
expect(boxplotOperator(formData, queryObject)).toEqual(undefined);
});
test('should do tukey boxplot', () => {
expect(
boxplotOperator(
{
...formData,
whiskerOptions: 'Tukey',
},
queryObject,
),
).toEqual({
operation: 'boxplot',
options: {
whisker_type: 'tukey',
percentiles: undefined,
groupby: [],
metrics: ['count(*)', 'sum(val)'],
},
});
});
test('should do min/max boxplot', () => {
expect(
boxplotOperator(
{
...formData,
whiskerOptions: 'Min/max (no outliers)',
},
queryObject,
),
).toEqual({
operation: 'boxplot',
options: {
whisker_type: 'min/max',
percentiles: undefined,
groupby: [],
metrics: ['count(*)', 'sum(val)'],
},
});
});
test('should do percentile boxplot', () => {
expect(
boxplotOperator(
{
...formData,
whiskerOptions: '1/4 percentiles',
},
queryObject,
),
).toEqual({
operation: 'boxplot',
options: {
whisker_type: 'percentile',
percentiles: [1, 4],
groupby: [],
metrics: ['count(*)', 'sum(val)'],
},
});
});
test('should throw an error', () => {
expect(() =>
boxplotOperator(
{
...formData,
whiskerOptions: 'foobar',
},
queryObject,
),
).toThrow('Unsupported whisker type: foobar');
});

View File

@ -0,0 +1,46 @@
/**
* 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 { QueryObject, SqlaFormData } from '@superset-ui/core';
import { contributionOperator } from '../../../src';
const formData: SqlaFormData = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
granularity: 'month',
datasource: 'foo',
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
granularity: 'month',
};
test('should skip contributionOperator', () => {
expect(contributionOperator(formData, queryObject)).toEqual(undefined);
});
test('should do contributionOperator', () => {
expect(contributionOperator({ ...formData, contributionMode: 'row' }, queryObject)).toEqual({
operation: 'contribution',
options: {
orientation: 'row',
},
});
});

View File

@ -47,82 +47,80 @@ const queryObject: QueryObject = {
],
};
describe('pivotOperator', () => {
it('skip pivot', () => {
expect(pivotOperator(formData, queryObject)).toEqual(undefined);
expect(pivotOperator(formData, { ...queryObject, is_timeseries: false })).toEqual(undefined);
expect(pivotOperator(formData, { ...queryObject, is_timeseries: true, metrics: [] })).toEqual(
undefined,
);
});
test('skip pivot', () => {
expect(pivotOperator(formData, queryObject)).toEqual(undefined);
expect(pivotOperator(formData, { ...queryObject, is_timeseries: false })).toEqual(undefined);
expect(pivotOperator(formData, { ...queryObject, is_timeseries: true, metrics: [] })).toEqual(
undefined,
);
});
it('pivot by __timestamp without groupby', () => {
expect(pivotOperator(formData, { ...queryObject, is_timeseries: true })).toEqual({
operation: 'pivot',
options: {
index: ['__timestamp'],
columns: [],
aggregates: {
'count(*)': { operator: 'mean' },
'sum(val)': { operator: 'mean' },
},
drop_missing_columns: false,
test('pivot by __timestamp without groupby', () => {
expect(pivotOperator(formData, { ...queryObject, is_timeseries: true })).toEqual({
operation: 'pivot',
options: {
index: ['__timestamp'],
columns: [],
aggregates: {
'count(*)': { operator: 'mean' },
'sum(val)': { operator: 'mean' },
},
});
});
it('pivot by __timestamp with groupby', () => {
expect(
pivotOperator(formData, { ...queryObject, columns: ['foo', 'bar'], is_timeseries: true }),
).toEqual({
operation: 'pivot',
options: {
index: ['__timestamp'],
columns: ['foo', 'bar'],
aggregates: {
'count(*)': { operator: 'mean' },
'sum(val)': { operator: 'mean' },
},
drop_missing_columns: false,
},
});
});
it('timecompare in formdata', () => {
expect(
pivotOperator(
{
...formData,
comparison_type: 'values',
time_compare: ['1 year ago', '1 year later'],
},
{
...queryObject,
columns: ['foo', 'bar'],
is_timeseries: true,
},
),
).toEqual({
operation: 'pivot',
options: {
aggregates: {
'count(*)': { operator: 'mean' },
'count(*)__1 year ago': { operator: 'mean' },
'count(*)__1 year later': { operator: 'mean' },
'sum(val)': {
operator: 'mean',
},
'sum(val)__1 year ago': {
operator: 'mean',
},
'sum(val)__1 year later': {
operator: 'mean',
},
},
drop_missing_columns: false,
columns: ['foo', 'bar'],
index: ['__timestamp'],
},
});
drop_missing_columns: false,
},
});
});
test('pivot by __timestamp with groupby', () => {
expect(
pivotOperator(formData, { ...queryObject, columns: ['foo', 'bar'], is_timeseries: true }),
).toEqual({
operation: 'pivot',
options: {
index: ['__timestamp'],
columns: ['foo', 'bar'],
aggregates: {
'count(*)': { operator: 'mean' },
'sum(val)': { operator: 'mean' },
},
drop_missing_columns: false,
},
});
});
test('timecompare in formdata', () => {
expect(
pivotOperator(
{
...formData,
comparison_type: 'values',
time_compare: ['1 year ago', '1 year later'],
},
{
...queryObject,
columns: ['foo', 'bar'],
is_timeseries: true,
},
),
).toEqual({
operation: 'pivot',
options: {
aggregates: {
'count(*)': { operator: 'mean' },
'count(*)__1 year ago': { operator: 'mean' },
'count(*)__1 year later': { operator: 'mean' },
'sum(val)': {
operator: 'mean',
},
'sum(val)__1 year ago': {
operator: 'mean',
},
'sum(val)__1 year later': {
operator: 'mean',
},
},
drop_missing_columns: false,
columns: ['foo', 'bar'],
index: ['__timestamp'],
},
});
});

View File

@ -0,0 +1,64 @@
/**
* 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 { QueryObject, SqlaFormData } from '@superset-ui/core';
import { prophetOperator } from '../../../src';
const formData: SqlaFormData = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
time_grain_sqla: 'P1Y',
datasource: 'foo',
viz_type: 'table',
};
const queryObject: QueryObject = {
metrics: ['count(*)', { label: 'sum(val)', expressionType: 'SQL', sqlExpression: 'sum(val)' }],
time_range: '2015 : 2016',
granularity: 'P1Y',
};
test('should skip prophetOperator', () => {
expect(prophetOperator(formData, queryObject)).toEqual(undefined);
});
test('should do prophetOperator', () => {
expect(
prophetOperator(
{
...formData,
forecastEnabled: true,
forecastPeriods: '3',
forecastInterval: '5',
forecastSeasonalityYearly: true,
forecastSeasonalityWeekly: false,
forecastSeasonalityDaily: false,
},
queryObject,
),
).toEqual({
operation: 'prophet',
options: {
time_grain: 'P1Y',
periods: 3.0,
confidence_interval: 5.0,
yearly_seasonality: true,
weekly_seasonality: false,
daily_seasonality: false,
},
});
});

View File

@ -46,43 +46,41 @@ const queryObject: QueryObject = {
],
};
describe('resampleOperator', () => {
it('should skip resampleOperator', () => {
expect(resampleOperator(formData, queryObject)).toEqual(undefined);
expect(resampleOperator({ ...formData, resample_method: 'ffill' }, queryObject)).toEqual(
undefined,
);
expect(resampleOperator({ ...formData, resample_rule: '1D' }, queryObject)).toEqual(undefined);
});
test('should skip resampleOperator', () => {
expect(resampleOperator(formData, queryObject)).toEqual(undefined);
expect(resampleOperator({ ...formData, resample_method: 'ffill' }, queryObject)).toEqual(
undefined,
);
expect(resampleOperator({ ...formData, resample_rule: '1D' }, queryObject)).toEqual(undefined);
});
it('should do resample', () => {
expect(
resampleOperator({ ...formData, resample_method: 'ffill', resample_rule: '1D' }, queryObject),
).toEqual({
operation: 'resample',
options: {
method: 'ffill',
rule: '1D',
fill_value: null,
time_column: '__timestamp',
},
});
});
it('should do zerofill resample', () => {
expect(
resampleOperator(
{ ...formData, resample_method: 'zerofill', resample_rule: '1D' },
queryObject,
),
).toEqual({
operation: 'resample',
options: {
method: 'asfreq',
rule: '1D',
fill_value: 0,
time_column: '__timestamp',
},
});
test('should do resample', () => {
expect(
resampleOperator({ ...formData, resample_method: 'ffill', resample_rule: '1D' }, queryObject),
).toEqual({
operation: 'resample',
options: {
method: 'ffill',
rule: '1D',
fill_value: null,
time_column: '__timestamp',
},
});
});
test('should do zerofill resample', () => {
expect(
resampleOperator(
{ ...formData, resample_method: 'zerofill', resample_rule: '1D' },
queryObject,
),
).toEqual({
operation: 'resample',
options: {
method: 'asfreq',
rule: '1D',
fill_value: 0,
time_column: '__timestamp',
},
});
});

View File

@ -46,26 +46,42 @@ const queryObject: QueryObject = {
],
};
describe('rollingWindowOperator', () => {
it('skip transformation', () => {
expect(rollingWindowOperator(formData, queryObject)).toEqual(undefined);
expect(rollingWindowOperator({ ...formData, rolling_type: 'None' }, queryObject)).toEqual(
undefined,
);
expect(rollingWindowOperator({ ...formData, rolling_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
test('skip transformation', () => {
expect(rollingWindowOperator(formData, queryObject)).toEqual(undefined);
expect(rollingWindowOperator({ ...formData, rolling_type: 'None' }, queryObject)).toEqual(
undefined,
);
expect(rollingWindowOperator({ ...formData, rolling_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
const formDataWithoutMetrics = { ...formData };
delete formDataWithoutMetrics.metrics;
expect(rollingWindowOperator(formDataWithoutMetrics, queryObject)).toEqual(undefined);
const formDataWithoutMetrics = { ...formData };
delete formDataWithoutMetrics.metrics;
expect(rollingWindowOperator(formDataWithoutMetrics, queryObject)).toEqual(undefined);
});
test('rolling_type: cumsum', () => {
expect(rollingWindowOperator({ ...formData, rolling_type: 'cumsum' }, queryObject)).toEqual({
operation: 'cum',
options: {
operator: 'sum',
columns: {
'count(*)': 'count(*)',
'sum(val)': 'sum(val)',
},
},
});
});
it('rolling_type: cumsum', () => {
expect(rollingWindowOperator({ ...formData, rolling_type: 'cumsum' }, queryObject)).toEqual({
operation: 'cum',
test('rolling_type: sum/mean/std', () => {
const rollingTypes = ['sum', 'mean', 'std'];
rollingTypes.forEach(rollingType => {
expect(rollingWindowOperator({ ...formData, rolling_type: rollingType }, queryObject)).toEqual({
operation: 'rolling',
options: {
operator: 'sum',
rolling_type: rollingType,
window: 1,
min_periods: 0,
columns: {
'count(*)': 'count(*)',
'sum(val)': 'sum(val)',
@ -73,34 +89,44 @@ describe('rollingWindowOperator', () => {
},
});
});
});
it('rolling_type: sum/mean/std', () => {
const rollingTypes = ['sum', 'mean', 'std'];
rollingTypes.forEach(rollingType => {
expect(
rollingWindowOperator({ ...formData, rolling_type: rollingType }, queryObject),
).toEqual({
operation: 'rolling',
options: {
rolling_type: rollingType,
window: 1,
min_periods: 0,
columns: {
'count(*)': 'count(*)',
'sum(val)': 'sum(val)',
},
},
});
});
test('rolling window and "actual values" in the time compare', () => {
expect(
rollingWindowOperator(
{
...formData,
rolling_type: 'cumsum',
comparison_type: 'values',
time_compare: ['1 year ago', '1 year later'],
},
queryObject,
),
).toEqual({
operation: 'cum',
options: {
operator: 'sum',
columns: {
'count(*)': 'count(*)',
'count(*)__1 year ago': 'count(*)__1 year ago',
'count(*)__1 year later': 'count(*)__1 year later',
'sum(val)': 'sum(val)',
'sum(val)__1 year ago': 'sum(val)__1 year ago',
'sum(val)__1 year later': 'sum(val)__1 year later',
},
},
});
});
it('rolling window and "actual values" in the time compare', () => {
test('rolling window and "absolute / percentage / ratio" in the time compare', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
rollingWindowOperator(
{
...formData,
rolling_type: 'cumsum',
comparison_type: 'values',
comparison_type: cType,
time_compare: ['1 year ago', '1 year later'],
},
queryObject,
@ -110,42 +136,12 @@ describe('rollingWindowOperator', () => {
options: {
operator: 'sum',
columns: {
'count(*)': 'count(*)',
'count(*)__1 year ago': 'count(*)__1 year ago',
'count(*)__1 year later': 'count(*)__1 year later',
'sum(val)': 'sum(val)',
'sum(val)__1 year ago': 'sum(val)__1 year ago',
'sum(val)__1 year later': 'sum(val)__1 year later',
[`${cType}__count(*)__count(*)__1 year ago`]: `${cType}__count(*)__count(*)__1 year ago`,
[`${cType}__count(*)__count(*)__1 year later`]: `${cType}__count(*)__count(*)__1 year later`,
[`${cType}__sum(val)__sum(val)__1 year ago`]: `${cType}__sum(val)__sum(val)__1 year ago`,
[`${cType}__sum(val)__sum(val)__1 year later`]: `${cType}__sum(val)__sum(val)__1 year later`,
},
},
});
});
it('rolling window and "absolute / percentage / ratio" in the time compare', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
rollingWindowOperator(
{
...formData,
rolling_type: 'cumsum',
comparison_type: cType,
time_compare: ['1 year ago', '1 year later'],
},
queryObject,
),
).toEqual({
operation: 'cum',
options: {
operator: 'sum',
columns: {
[`${cType}__count(*)__count(*)__1 year ago`]: `${cType}__count(*)__count(*)__1 year ago`,
[`${cType}__count(*)__count(*)__1 year later`]: `${cType}__count(*)__count(*)__1 year later`,
[`${cType}__sum(val)__sum(val)__1 year ago`]: `${cType}__sum(val)__sum(val)__1 year ago`,
[`${cType}__sum(val)__sum(val)__1 year later`]: `${cType}__sum(val)__sum(val)__1 year later`,
},
},
});
});
});
});

View File

@ -46,62 +46,57 @@ const queryObject: QueryObject = {
],
};
describe('sortOperator', () => {
it('skip sort', () => {
expect(sortOperator(formData, queryObject)).toEqual(undefined);
expect(sortOperator(formData, { ...queryObject, is_timeseries: false })).toEqual(undefined);
expect(
sortOperator({ ...formData, rolling_type: 'xxxx' }, { ...queryObject, is_timeseries: true }),
).toEqual(undefined);
expect(sortOperator(formData, { ...queryObject, is_timeseries: true })).toEqual(undefined);
test('skip sort', () => {
expect(sortOperator(formData, queryObject)).toEqual(undefined);
expect(sortOperator(formData, { ...queryObject, is_timeseries: false })).toEqual(undefined);
expect(
sortOperator({ ...formData, rolling_type: 'xxxx' }, { ...queryObject, is_timeseries: true }),
).toEqual(undefined);
expect(sortOperator(formData, { ...queryObject, is_timeseries: true })).toEqual(undefined);
});
test('sort by __timestamp', () => {
expect(
sortOperator({ ...formData, rolling_type: 'cumsum' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
},
});
it('sort by __timestamp', () => {
expect(
sortOperator(
{ ...formData, rolling_type: 'cumsum' },
{ ...queryObject, is_timeseries: true },
),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
expect(
sortOperator({ ...formData, rolling_type: 'sum' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
});
},
});
expect(
sortOperator({ ...formData, rolling_type: 'sum' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
expect(
sortOperator({ ...formData, rolling_type: 'mean' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
});
},
});
expect(
sortOperator({ ...formData, rolling_type: 'mean' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
expect(
sortOperator({ ...formData, rolling_type: 'std' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
});
expect(
sortOperator({ ...formData, rolling_type: 'std' }, { ...queryObject, is_timeseries: true }),
).toEqual({
operation: 'sort',
options: {
columns: {
__timestamp: true,
},
},
});
},
});
});

View File

@ -54,70 +54,91 @@ const queryObject: QueryObject = {
],
};
describe('timeCompare', () => {
it('time compare: skip transformation', () => {
expect(timeCompareOperator(formData, queryObject)).toEqual(undefined);
expect(timeCompareOperator({ ...formData, time_compare: [] }, queryObject)).toEqual(undefined);
expect(timeCompareOperator({ ...formData, comparison_type: null }, queryObject)).toEqual(
undefined,
);
expect(timeCompareOperator({ ...formData, comparison_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
test('time compare: skip transformation', () => {
expect(timeCompareOperator(formData, queryObject)).toEqual(undefined);
expect(timeCompareOperator({ ...formData, time_compare: [] }, queryObject)).toEqual(undefined);
expect(timeCompareOperator({ ...formData, comparison_type: null }, queryObject)).toEqual(
undefined,
);
expect(timeCompareOperator({ ...formData, comparison_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
expect(
timeCompareOperator(
{ ...formData, comparison_type: 'values', time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual(undefined);
});
test('time compare: absolute/percentage/ratio', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
timeCompareOperator(
{ ...formData, comparison_type: 'values', time_compare: ['1 year ago', '1 year later'] },
{ ...formData, comparison_type: cType, time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual(undefined);
});
it('time compare: absolute/percentage/ratio', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
timeCompareOperator(
{ ...formData, comparison_type: cType, time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual({
operation: 'compare',
options: {
source_columns: ['count(*)', 'count(*)'],
compare_columns: ['count(*)__1 year ago', 'count(*)__1 year later'],
compare_type: cType,
drop_original_columns: true,
},
});
).toEqual({
operation: 'compare',
options: {
source_columns: ['count(*)', 'count(*)'],
compare_columns: ['count(*)__1 year ago', 'count(*)__1 year later'],
compare_type: cType,
drop_original_columns: true,
},
});
});
});
it('time compare pivot: skip transformation', () => {
expect(timeComparePivotOperator(formData, queryObject)).toEqual(undefined);
expect(timeComparePivotOperator({ ...formData, time_compare: [] }, queryObject)).toEqual(
undefined,
);
expect(timeComparePivotOperator({ ...formData, comparison_type: null }, queryObject)).toEqual(
undefined,
);
expect(timeCompareOperator({ ...formData, comparison_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
test('time compare pivot: skip transformation', () => {
expect(timeComparePivotOperator(formData, queryObject)).toEqual(undefined);
expect(timeComparePivotOperator({ ...formData, time_compare: [] }, queryObject)).toEqual(
undefined,
);
expect(timeComparePivotOperator({ ...formData, comparison_type: null }, queryObject)).toEqual(
undefined,
);
expect(timeCompareOperator({ ...formData, comparison_type: 'foobar' }, queryObject)).toEqual(
undefined,
);
});
test('time compare pivot: values', () => {
expect(
timeComparePivotOperator(
{ ...formData, comparison_type: 'values', time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual({
operation: 'pivot',
options: {
aggregates: {
'count(*)': { operator: 'mean' },
'count(*)__1 year ago': { operator: 'mean' },
'count(*)__1 year later': { operator: 'mean' },
},
drop_missing_columns: false,
columns: [],
index: ['__timestamp'],
},
});
});
it('time compare pivot: values', () => {
test('time compare pivot: absolute/percentage/ratio', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
timeComparePivotOperator(
{ ...formData, comparison_type: 'values', time_compare: ['1 year ago', '1 year later'] },
{ ...formData, comparison_type: cType, time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual({
operation: 'pivot',
options: {
aggregates: {
'count(*)': { operator: 'mean' },
'count(*)__1 year ago': { operator: 'mean' },
'count(*)__1 year later': { operator: 'mean' },
[`${cType}__count(*)__count(*)__1 year ago`]: { operator: 'mean' },
[`${cType}__count(*)__count(*)__1 year later`]: { operator: 'mean' },
},
drop_missing_columns: false,
columns: [],
@ -125,27 +146,4 @@ describe('timeCompare', () => {
},
});
});
it('time compare pivot: absolute/percentage/ratio', () => {
const comparisionTypes = ['absolute', 'percentage', 'ratio'];
comparisionTypes.forEach(cType => {
expect(
timeComparePivotOperator(
{ ...formData, comparison_type: cType, time_compare: ['1 year ago', '1 year later'] },
queryObject,
),
).toEqual({
operation: 'pivot',
options: {
aggregates: {
[`${cType}__count(*)__count(*)__1 year ago`]: { operator: 'mean' },
[`${cType}__count(*)__count(*)__1 year later`]: { operator: 'mean' },
},
drop_missing_columns: false,
columns: [],
index: ['__timestamp'],
},
});
});
});
});

View File

@ -16,31 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { buildQueryContext, getMetricLabel } from '@superset-ui/core';
import { BoxPlotQueryFormData, BoxPlotQueryObjectWhiskerType } from './types';
const PERCENTILE_REGEX = /(\d+)\/(\d+) percentiles/;
import { buildQueryContext } from '@superset-ui/core';
import { boxplotOperator } from '@superset-ui/chart-controls';
import { BoxPlotQueryFormData } from './types';
export default function buildQuery(formData: BoxPlotQueryFormData) {
const { columns = [], granularity_sqla, groupby = [], whiskerOptions } = formData;
const { columns = [], granularity_sqla, groupby = [] } = formData;
return buildQueryContext(formData, baseQueryObject => {
let whiskerType: BoxPlotQueryObjectWhiskerType;
let percentiles: [number, number] | undefined;
const { metrics = [] } = baseQueryObject;
const percentileMatch = PERCENTILE_REGEX.exec(whiskerOptions as string);
if (whiskerOptions === 'Tukey' || !whiskerOptions) {
whiskerType = 'tukey';
} else if (whiskerOptions === 'Min/max (no outliers)') {
whiskerType = 'min/max';
} else if (percentileMatch) {
whiskerType = 'percentile';
percentiles = [parseInt(percentileMatch[1], 10), parseInt(percentileMatch[2], 10)];
} else {
throw new Error(`Unsupported whisker type: ${whiskerOptions}`);
}
const distributionColumns: string[] = [];
// For now default to using the temporal column as distribution column.
// In the future this control should be made mandatory.
if (!columns.length && granularity_sqla) {
@ -51,17 +34,7 @@ export default function buildQuery(formData: BoxPlotQueryFormData) {
...baseQueryObject,
columns: [...distributionColumns, ...columns, ...groupby],
series_columns: groupby,
post_processing: [
{
operation: 'boxplot',
options: {
whisker_type: whiskerType,
percentiles,
groupby,
metrics: metrics.map(getMetricLabel),
},
},
],
post_processing: [boxplotOperator(formData, baseQueryObject)],
},
];
});

View File

@ -23,7 +23,6 @@ import {
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { PostProcessingBoxplot } from '@superset-ui/core/lib/query/types/PostProcessing';
import { EChartsOption } from 'echarts';
import { EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA } from '../types';
@ -53,8 +52,6 @@ export interface EchartsBoxPlotChartProps extends ChartProps {
queriesData: ChartDataResponseResult[];
}
export type BoxPlotQueryObjectWhiskerType = PostProcessingBoxplot['options']['whisker_type'];
export interface BoxPlotChartTransformedProps {
formData: BoxPlotQueryFormData;
height: number;

View File

@ -24,6 +24,8 @@ import {
sortOperator,
pivotOperator,
resampleOperator,
contributionOperator,
prophetOperator,
} from '@superset-ui/chart-controls';
export default function buildQuery(formData: QueryFormData) {
@ -40,27 +42,8 @@ export default function buildQuery(formData: QueryFormData) {
sortOperator(formData, { ...baseQueryObject, is_timeseries: true }),
rollingWindowOperator(formData, baseQueryObject),
pivotOperator(formData, { ...baseQueryObject, is_timeseries: true }),
formData.contributionMode
? {
operation: 'contribution',
options: {
orientation: formData.contributionMode,
},
}
: undefined,
formData.forecastEnabled
? {
operation: 'prophet',
options: {
time_grain: formData.time_grain_sqla,
periods: parseInt(formData.forecastPeriods, 10),
confidence_interval: parseFloat(formData.forecastInterval),
yearly_seasonality: formData.forecastSeasonalityYearly,
weekly_seasonality: formData.forecastSeasonalityWeekly,
daily_seasonality: formData.forecastSeasonalityDaily,
},
}
: undefined,
contributionOperator(formData, baseQueryObject),
prophetOperator(formData, baseQueryObject),
],
},
]);