diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts index dda617ab62b82..681203995034f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/index.ts @@ -20,6 +20,7 @@ import * as sectionsModule from './sections'; export * from './utils'; export * from './constants'; +export * from './operators'; // can't do `export * as sections from './sections'`, babel-transformer will fail export const sections = sectionsModule; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/index.ts new file mode 100644 index 0000000000000..916d0085e376e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/index.ts @@ -0,0 +1,25 @@ +/* eslint-disable camelcase */ +/** + * 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. + */ +export { rollingWindowOperator } from './rollingWindowOperator'; +export { timeCompareOperator } from './timeCompareOperator'; +export { timeComparePivotOperator } from './timeComparePivotOperator'; +export { sortOperator } from './sortOperator'; +export { pivotOperator } from './pivotOperator'; +export * from './utils'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/pivotOperator.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/pivotOperator.ts new file mode 100644 index 0000000000000..45e5a1d173825 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/pivotOperator.ts @@ -0,0 +1,48 @@ +/** + * 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 { ensureIsArray, getMetricLabel, PostProcessingPivot } from '@superset-ui/core'; +import { PostProcessingFactory } from './types'; +import { TIME_COLUMN, isValidTimeCompare } from './utils'; +import { timeComparePivotOperator } from './timeComparePivotOperator'; + +export const pivotOperator: PostProcessingFactory = ( + formData, + queryObject, +) => { + const metricLabels = ensureIsArray(queryObject.metrics).map(getMetricLabel); + if (queryObject.is_timeseries && metricLabels.length) { + if (isValidTimeCompare(formData, queryObject)) { + return timeComparePivotOperator(formData, queryObject); + } + + return { + operation: 'pivot', + options: { + index: [TIME_COLUMN], + columns: queryObject.columns || [], + // Create 'dummy' mean aggregates to assign cell values in pivot table + // use the 'mean' aggregates to avoid drop NaN. PR: https://github.com/apache-superset/superset-ui/pull/1231 + aggregates: Object.fromEntries(metricLabels.map(metric => [metric, { operator: 'mean' }])), + drop_missing_columns: false, + }, + }; + } + + return undefined; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/rollingWindowOperator.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/rollingWindowOperator.ts new file mode 100644 index 0000000000000..48550385a4af4 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/rollingWindowOperator.ts @@ -0,0 +1,80 @@ +/* eslint-disable camelcase */ +/** + * 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 { + ensureIsInt, + ensureIsArray, + RollingType, + PostProcessingRolling, + PostProcessingCum, + ComparisionType, +} from '@superset-ui/core'; +import { getMetricOffsetsMap, isValidTimeCompare, TIME_COMPARISON_SEPARATOR } from './utils'; +import { PostProcessingFactory } from './types'; + +export const rollingWindowOperator: PostProcessingFactory< + PostProcessingRolling | PostProcessingCum | undefined +> = (formData, queryObject) => { + let columns: (string | undefined)[]; + if (isValidTimeCompare(formData, queryObject)) { + const metricsMap = getMetricOffsetsMap(formData, queryObject); + const comparisonType = formData.comparison_type; + if (comparisonType === ComparisionType.Values) { + // time compare type: actual values + columns = [...Array.from(metricsMap.values()), ...Array.from(metricsMap.keys())]; + } else { + // time compare type: absolute / percentage / ratio + columns = Array.from(metricsMap.entries()).map(([offset, metric]) => + [comparisonType, metric, offset].join(TIME_COMPARISON_SEPARATOR), + ); + } + } else { + columns = ensureIsArray(queryObject.metrics).map(metric => { + if (typeof metric === 'string') { + return metric; + } + return metric.label; + }); + } + const columnsMap = Object.fromEntries(columns.map(col => [col, col])); + + if (formData.rolling_type === RollingType.Cumsum) { + return { + operation: 'cum', + options: { + operator: 'sum', + columns: columnsMap, + }, + }; + } + + if ([RollingType.Sum, RollingType.Mean, RollingType.Std].includes(formData.rolling_type)) { + return { + operation: 'rolling', + options: { + rolling_type: formData.rolling_type, + window: ensureIsInt(formData.rolling_periods, 1), + min_periods: ensureIsInt(formData.min_periods, 0), + columns: columnsMap, + }, + }; + } + + return undefined; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/sortOperator.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/sortOperator.ts new file mode 100644 index 0000000000000..3ab04faad5151 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/sortOperator.ts @@ -0,0 +1,39 @@ +/* eslint-disable camelcase */ +/** + * 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 { PostProcessingSort, RollingType } from '@superset-ui/core'; +import { PostProcessingFactory } from './types'; +import { TIME_COLUMN } from './utils'; + +export const sortOperator: PostProcessingFactory = ( + formData, + queryObject, +) => { + if (queryObject.is_timeseries && Object.values(RollingType).includes(formData.rolling_type)) { + return { + operation: 'sort', + options: { + columns: { + [TIME_COLUMN]: true, + }, + }, + }; + } + return undefined; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeCompareOperator.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeCompareOperator.ts new file mode 100644 index 0000000000000..1680f49783d4a --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeCompareOperator.ts @@ -0,0 +1,44 @@ +/* eslint-disable camelcase */ +/** + * 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 { ComparisionType, PostProcessingCompare } from '@superset-ui/core'; +import { getMetricOffsetsMap, isValidTimeCompare } from './utils'; +import { PostProcessingFactory } from './types'; + +export const timeCompareOperator: PostProcessingFactory = ( + formData, + queryObject, +) => { + const comparisonType = formData.comparison_type; + const metricOffsetMap = getMetricOffsetsMap(formData, queryObject); + + if (isValidTimeCompare(formData, queryObject) && comparisonType !== ComparisionType.Values) { + return { + operation: 'compare', + options: { + source_columns: Array.from(metricOffsetMap.values()), + compare_columns: Array.from(metricOffsetMap.keys()), + compare_type: comparisonType, + drop_original_columns: true, + }, + }; + } + + return undefined; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeComparePivotOperator.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeComparePivotOperator.ts new file mode 100644 index 0000000000000..74c9c90207d4f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/timeComparePivotOperator.ts @@ -0,0 +1,58 @@ +/* eslint-disable camelcase */ +/** + * 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 { ComparisionType, PostProcessingPivot, NumpyFunction } from '@superset-ui/core'; +import { getMetricOffsetsMap, isValidTimeCompare, TIME_COMPARISON_SEPARATOR } from './utils'; +import { PostProcessingFactory } from './types'; + +export const timeComparePivotOperator: PostProcessingFactory = ( + formData, + queryObject, +) => { + const comparisonType = formData.comparison_type; + const metricOffsetMap = getMetricOffsetsMap(formData, queryObject); + + if (isValidTimeCompare(formData, queryObject)) { + const valuesAgg = Object.fromEntries( + [...metricOffsetMap.values(), ...metricOffsetMap.keys()].map(metric => [ + metric, + // use the 'mean' aggregates to avoid drop NaN + { operator: 'mean' as NumpyFunction }, + ]), + ); + const changeAgg = Object.fromEntries( + [...metricOffsetMap.entries()] + .map(([offset, metric]) => [comparisonType, metric, offset].join(TIME_COMPARISON_SEPARATOR)) + // use the 'mean' aggregates to avoid drop NaN + .map(metric => [metric, { operator: 'mean' as NumpyFunction }]), + ); + + return { + operation: 'pivot', + options: { + index: ['__timestamp'], + columns: queryObject.columns || [], + aggregates: comparisonType === ComparisionType.Values ? valuesAgg : changeAgg, + drop_missing_columns: false, + }, + }; + } + + return undefined; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/types.ts new file mode 100644 index 0000000000000..34f632ff8f38f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/types.ts @@ -0,0 +1,23 @@ +/** + * 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 { QueryFormData, QueryObject } from '@superset-ui/core'; + +export interface PostProcessingFactory { + (formData: QueryFormData, queryObject: QueryObject): T; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/constants.ts new file mode 100644 index 0000000000000..cb01a2e6c5cb7 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/constants.ts @@ -0,0 +1,21 @@ +/* eslint-disable camelcase */ +/** + * 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. + */ +export const TIME_COMPARISON_SEPARATOR = '__'; +export const TIME_COLUMN = '__timestamp'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/getMetricOffsetsMap.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/getMetricOffsetsMap.ts new file mode 100644 index 0000000000000..dbe268a2b7092 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/getMetricOffsetsMap.ts @@ -0,0 +1,47 @@ +/* eslint-disable camelcase */ +/** + * 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 { getMetricLabel, ensureIsArray } from '@superset-ui/core'; +import { PostProcessingFactory } from '../types'; +import { TIME_COMPARISON_SEPARATOR } from './constants'; + +export const getMetricOffsetsMap: PostProcessingFactory> = ( + formData, + queryObject, +) => { + /* + return metric offset-label and metric-label hashmap, for instance: + { + "SUM(value)__1 year ago": "SUM(value)", + "SUM(value)__2 year ago": "SUM(value)" + } + */ + const queryMetrics = ensureIsArray(queryObject.metrics); + const timeOffsets = ensureIsArray(formData.time_compare); + + const metricLabels = queryMetrics.map(getMetricLabel); + const metricOffsetMap = new Map(); + metricLabels.forEach((metric: string) => { + timeOffsets.forEach((offset: string) => { + metricOffsetMap.set([metric, offset].join(TIME_COMPARISON_SEPARATOR), metric); + }); + }); + + return metricOffsetMap; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/index.ts new file mode 100644 index 0000000000000..acf072f74678b --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/index.ts @@ -0,0 +1,22 @@ +/* eslint-disable camelcase */ +/** + * 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. + */ +export { getMetricOffsetsMap } from './getMetricOffsetsMap'; +export { isValidTimeCompare } from './isValidTimeCompare'; +export { TIME_COMPARISON_SEPARATOR, TIME_COLUMN } from './constants'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/isValidTimeCompare.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/isValidTimeCompare.ts new file mode 100644 index 0000000000000..a2a4afaa33925 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/operators/utils/isValidTimeCompare.ts @@ -0,0 +1,29 @@ +/* eslint-disable camelcase */ +/** + * 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 { ComparisionType } from '@superset-ui/core'; +import { getMetricOffsetsMap } from './getMetricOffsetsMap'; +import { PostProcessingFactory } from '../types'; + +export const isValidTimeCompare: PostProcessingFactory = (formData, queryObject) => { + const comparisonType = formData.comparison_type; + const metricOffsetMap = getMetricOffsetsMap(formData, queryObject); + + return Object.values(ComparisionType).includes(comparisonType) && metricOffsetMap.size > 0; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx new file mode 100644 index 0000000000000..d29f61e787b8e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx @@ -0,0 +1,130 @@ +/** + * 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 React from 'react'; +import { t, RollingType, ComparisionType } from '@superset-ui/core'; +import { ControlPanelSectionConfig } from '../types'; +import { formatSelectOptions } from '../utils'; + +export const advancedAnalytics: ControlPanelSectionConfig = { + label: t('Advanced analytics'), + tabOverride: 'data', + description: t( + 'This section contains options ' + + 'that allow for advanced analytical post processing ' + + 'of query results', + ), + controlSetRows: [ + [

{t('Rolling window')}

], + [ + { + name: 'rolling_type', + config: { + type: 'SelectControl', + label: t('Rolling function'), + default: null, + choices: [[null, t('None')]].concat(formatSelectOptions(Object.values(RollingType))), + description: t( + 'Defines a rolling window function to apply, works along ' + + 'with the [Periods] text box', + ), + }, + }, + ], + [ + { + name: 'rolling_periods', + config: { + type: 'TextControl', + label: t('Periods'), + isInt: true, + description: t( + 'Defines the size of the rolling window function, ' + + 'relative to the time granularity selected', + ), + }, + }, + ], + [ + { + name: 'min_periods', + config: { + type: 'TextControl', + label: t('Min periods'), + isInt: true, + description: t( + 'The minimum number of rolling periods required to show ' + + 'a value. For instance if you do a cumulative sum on 7 days ' + + 'you may want your "Min Period" to be 7, so that all data points ' + + 'shown are the total of 7 periods. This will hide the "ramp up" ' + + 'taking place over the first 7 periods', + ), + }, + }, + ], + [

{t('Time comparison')}

], + [ + { + name: 'time_compare', + config: { + type: 'SelectControl', + multi: true, + freeForm: true, + label: t('Time shift'), + choices: formatSelectOptions([ + '1 day ago', + '1 week ago', + '28 days ago', + '30 days ago', + '52 weeks ago', + '1 year ago', + '104 weeks ago', + '2 years ago', + ]), + description: t( + 'Overlay one or more timeseries from a ' + + 'relative time period. Expects relative time deltas ' + + 'in natural language (example: 24 hours, 7 days, ' + + '52 weeks, 365 days). Free text is supported.', + ), + }, + }, + ], + [ + { + name: 'comparison_type', + config: { + type: 'SelectControl', + label: t('Calculation type'), + default: 'values', + choices: [ + [ComparisionType.Values, 'Actual values'], + [ComparisionType.Absolute, 'Absolute difference'], + [ComparisionType.Percentage, 'Percentage change'], + [ComparisionType.Ratio, 'Ratio'], + ], + description: t( + 'How to display time shifts: as individual lines; as the ' + + 'absolute difference between the main time series and each time shift; ' + + 'as the percentage change; or as the ratio between series and time shifts.', + ), + }, + }, + ], + ], +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/index.ts new file mode 100644 index 0000000000000..a67ca219e1497 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/index.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ + +export * from './sections'; +export * from './advancedAnalytics'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/sections.tsx similarity index 98% rename from superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections.tsx rename to superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/sections.tsx index 41a5a87f8868d..eb93c469df812 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/sections/sections.tsx @@ -17,7 +17,7 @@ * under the License. */ import { t } from '@superset-ui/core'; -import { ControlPanelSectionConfig } from './types'; +import { ControlPanelSectionConfig } from '../types'; // A few standard controls sections that are used internally. // Not recommended for use in third-party plugins. diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/pivotOperator.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/pivotOperator.test.ts new file mode 100644 index 0000000000000..df2563e57775d --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/pivotOperator.test.ts @@ -0,0 +1,128 @@ +/** + * 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 { pivotOperator } 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', + post_processing: [ + { + operation: 'pivot', + options: { + index: ['__timestamp'], + columns: ['nation'], + aggregates: { + 'count(*)': { + operator: 'mean', + }, + }, + drop_missing_columns: false, + }, + }, + ], +}; + +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, + ); + }); + + 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, + }, + }); + }); + + 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'], + }, + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/rollingWindowOperator.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/rollingWindowOperator.test.ts new file mode 100644 index 0000000000000..c4e24afe52935 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/rollingWindowOperator.test.ts @@ -0,0 +1,151 @@ +/** + * 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 { rollingWindowOperator } 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', + post_processing: [ + { + operation: 'pivot', + options: { + index: ['__timestamp'], + columns: ['nation'], + aggregates: { + 'count(*)': { + operator: 'sum', + }, + }, + }, + }, + ], +}; + +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, + ); + + const formDataWithoutMetrics = { ...formData }; + delete formDataWithoutMetrics.metrics; + expect(rollingWindowOperator(formDataWithoutMetrics, queryObject)).toEqual(undefined); + }); + + it('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: 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)', + }, + }, + }); + }); + }); + + it('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 "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`, + }, + }, + }); + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/sortOperator.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/sortOperator.test.ts new file mode 100644 index 0000000000000..02d8cb060e72e --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/sortOperator.test.ts @@ -0,0 +1,107 @@ +/** + * 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 { sortOperator } 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', + post_processing: [ + { + operation: 'pivot', + options: { + index: ['__timestamp'], + columns: ['nation'], + aggregates: { + 'count(*)': { + operator: 'sum', + }, + }, + }, + }, + ], +}; + +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); + }); + + 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: '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, + }, + }, + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/timeCompareOperator.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/timeCompareOperator.test.ts new file mode 100644 index 0000000000000..3a9981f9e76c1 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/test/utils/operators/timeCompareOperator.test.ts @@ -0,0 +1,151 @@ +/** + * 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 { timeCompareOperator, timeComparePivotOperator } from '../../../src'; + +const formData: SqlaFormData = { + metrics: ['count(*)'], + time_range: '2015 : 2016', + granularity: 'month', + datasource: 'foo', + viz_type: 'table', +}; +const queryObject: QueryObject = { + metrics: ['count(*)'], + time_range: '2015 : 2016', + granularity: 'month', + post_processing: [ + { + operation: 'pivot', + options: { + index: ['__timestamp'], + columns: ['nation'], + aggregates: { + 'count(*)': { + operator: 'mean', + }, + }, + drop_missing_columns: false, + }, + }, + { + operation: 'aggregation', + options: { + groupby: ['col1'], + aggregates: 'count', + }, + }, + ], +}; + +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, + ); + expect( + timeCompareOperator( + { ...formData, comparison_type: 'values', 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, + }, + }); + }); + }); + + 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, + ); + }); + + it('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: 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'], + }, + }); + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/AdvancedAnalytics.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/AdvancedAnalytics.ts new file mode 100644 index 0000000000000..3574bc9b96033 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/AdvancedAnalytics.ts @@ -0,0 +1,43 @@ +/* eslint-disable camelcase */ +/** + * 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. + */ +export enum RollingType { + Mean = 'mean', + Sum = 'sum', + Std = 'std', + Cumsum = 'cumsum', +} +export interface RollingWindow { + rolling_type?: RollingType; + rolling_periods?: number; + min_periods?: number; +} + +export enum ComparisionType { + Values = 'values', + Absolute = 'absolute', + Percentage = 'percentage', + Ratio = 'ratio', +} +export interface TimeCompare { + time_compare?: string; + comparison_type?: ComparisionType; +} + +export default {}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/PostProcessing.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/PostProcessing.ts index 76b0419f9fb45..c52d10ddfceb2 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/PostProcessing.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/PostProcessing.ts @@ -18,6 +18,7 @@ */ import { JsonObject } from '../../connection'; import { TimeGranularity } from '../../time-format'; +import { RollingType, ComparisionType } from './AdvancedAnalytics'; export type NumpyFunction = | 'average' @@ -44,6 +45,11 @@ export type NumpyFunction = | 'sum' | 'var'; +export enum PandasAxis { + Row = 0, + Column = 1, +} + interface Aggregates { /** * The name of the generated aggregate column. @@ -106,6 +112,50 @@ export interface PostProcessingProphet { }; } +export interface PostProcessingDiff { + operation: 'diff'; + options: { + columns: string[]; + periods: number; + axis: PandasAxis; + }; +} + +export interface PostProcessingRolling { + operation: 'rolling'; + options: { + rolling_type: RollingType; + window: number; + min_periods: number; + columns: string[]; + }; +} + +export interface PostProcessingCum { + operation: 'cum'; + options: { + columns: string[]; + operator: NumpyFunction; + }; +} + +export interface PostProcessingCompare { + operation: 'compare'; + options: { + source_columns: string[]; + compare_columns: string[]; + compare_type: Omit; + drop_original_columns: boolean; + }; +} + +export interface PostProcessingSort { + operation: 'sort'; + options: { + columns: Record; + }; +} + /** * Parameters for chart data postprocessing. * See superset/utils/pandas_processing.py. @@ -115,4 +165,9 @@ export type PostProcessingRule = | PostProcessingBoxplot | PostProcessingContribution | PostProcessingPivot - | PostProcessingProphet; + | PostProcessingProphet + | PostProcessingDiff + | PostProcessingRolling + | PostProcessingCum + | PostProcessingCompare + | PostProcessingSort; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/index.ts index dd6a902f60bd1..2ee4427efdf40 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/query/types/index.ts @@ -25,6 +25,8 @@ export * from './Query'; export * from './QueryFormData'; export * from './QueryResponse'; export * from './Time'; +export * from './AdvancedAnalytics'; +export * from './PostProcessing'; export { default as __hack_reexport_Datasource } from './Datasource'; export { default as __hack_reexport_Column } from './Column'; @@ -33,5 +35,6 @@ export { default as __hack_reexport_Query } from './Query'; export { default as __hack_reexport_QueryResponse } from './QueryResponse'; export { default as __hack_reexport_QueryFormData } from './QueryFormData'; export { default as __hack_reexport_Time } from './Time'; +export { default as __hack_reexport_AdvancedAnalytics } from './AdvancedAnalytics'; export default {}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/ensureIsInt.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/ensureIsInt.ts new file mode 100644 index 0000000000000..dfa67358fa227 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/ensureIsInt.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ + +export default function ensureIsInt(value: T, defaultValue?: number): number { + const val = parseInt(String(value), 10); + const defaultOrNaN = defaultValue === undefined ? NaN : defaultValue; + return Number.isNaN(val) ? defaultOrNaN : val; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/index.ts index dd7524deb5837..1caff909d98eb 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/src/utils/index.ts @@ -18,6 +18,7 @@ */ export { default as convertKeysToCamelCase } from './convertKeysToCamelCase'; export { default as ensureIsArray } from './ensureIsArray'; +export { default as ensureIsInt } from './ensureIsInt'; export { default as isDefined } from './isDefined'; export { default as isRequired } from './isRequired'; export { default as makeSingleton } from './makeSingleton'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/test/utils/ensureIsInt.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/test/utils/ensureIsInt.test.ts new file mode 100644 index 0000000000000..0558d7d93a824 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-core/test/utils/ensureIsInt.test.ts @@ -0,0 +1,31 @@ +/** + * 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 { ensureIsInt } from '../../src'; + +describe('ensureIsInt', () => { + it('handle inputs correctly', () => { + expect(ensureIsInt(undefined, 0)).toEqual(0); + expect(ensureIsInt('abc', 1)).toEqual(1); + expect(ensureIsInt(undefined)).toEqual(NaN); + expect(ensureIsInt('abc')).toEqual(NaN); + expect(ensureIsInt('12.5')).toEqual(12); + expect(ensureIsInt(12)).toEqual(12); + expect(ensureIsInt(12, 0)).toEqual(12); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts index c9dd3a96e5aed..2c015a651b9f5 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts @@ -16,59 +16,50 @@ * specific language governing permissions and limitations * under the License. */ -import { buildQueryContext, getMetricLabel, QueryFormData } from '@superset-ui/core'; +import { buildQueryContext, QueryFormData, normalizeOrderBy } from '@superset-ui/core'; +import { + rollingWindowOperator, + timeCompareOperator, + isValidTimeCompare, + sortOperator, + pivotOperator, +} from '@superset-ui/chart-controls'; export default function buildQuery(formData: QueryFormData) { - return buildQueryContext(formData, baseQueryObject => { - const metricLabels = (baseQueryObject.metrics || []).map(getMetricLabel); - const { timeseries_limit_metric, order_desc, orderby } = baseQueryObject; - return [ - { - ...baseQueryObject, - groupby: formData.groupby || [], - is_timeseries: true, - orderby: orderby?.length - ? orderby - : timeseries_limit_metric - ? [[timeseries_limit_metric, !order_desc]] - : [], - post_processing: [ - { - operation: 'pivot', - options: { - index: ['__timestamp'], - columns: formData.groupby || [], - // Create 'dummy' mean aggregates to assign cell values in pivot table - // use the 'mean' aggregates to avoid drop NaN - aggregates: Object.fromEntries( - metricLabels.map(metric => [metric, { operator: 'mean' }]), - ), - drop_missing_columns: false, - }, - }, - 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, - ], - }, - ]; - }); + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + is_timeseries: true, + // todo: move `normalizeOrderBy to extractQueryFields` + orderby: normalizeOrderBy(baseQueryObject).orderby, + time_offsets: isValidTimeCompare(formData, baseQueryObject) ? formData.time_compare : [], + post_processing: [ + timeCompareOperator(formData, baseQueryObject), + 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, + ], + }, + ]); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx index 51e63daa99922..28a5dfa27175f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx @@ -106,6 +106,7 @@ const config: ControlPanelConfig = { ['row_limit'], ], }, + sections.advancedAnalytics, { label: t('Annotations and Layers'), expanded: false, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts index 1969b27924839..7c0787dc0241d 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts @@ -22,7 +22,6 @@ describe('Timeseries buildQuery', () => { const formData = { datasource: '5__table', granularity_sqla: 'ds', - groupby: ['foo'], metrics: ['bar', 'baz'], viz_type: 'my_chart', }; @@ -30,7 +29,6 @@ describe('Timeseries buildQuery', () => { it('should build groupby with series in form data', () => { const queryContext = buildQuery(formData); const [query] = queryContext.queries; - expect(query.groupby).toEqual(['foo']); expect(query.metrics).toEqual(['bar', 'baz']); }); @@ -41,7 +39,6 @@ describe('Timeseries buildQuery', () => { order_desc: true, }); const [query] = queryContext.queries; - expect(query.groupby).toEqual(['foo']); expect(query.metrics).toEqual(['bar', 'baz']); expect(query.timeseries_limit_metric).toEqual('bar'); expect(query.order_desc).toEqual(true); @@ -56,7 +53,6 @@ describe('Timeseries buildQuery', () => { orderby: [['foo', true]], }); const [query] = queryContext.queries; - expect(query.groupby).toEqual(['foo']); expect(query.metrics).toEqual(['bar', 'baz']); expect(query.timeseries_limit_metric).toEqual('bar'); expect(query.order_desc).toEqual(true);