Skip to content

Commit

Permalink
feat: add Advanced Analytics into mixed time series chart (apache#19851)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhaoyongjie committed May 11, 2022
1 parent 793073f commit f157bc8
Show file tree
Hide file tree
Showing 7 changed files with 503 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,16 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
'Defines the size of the rolling window function, ' +
'relative to the time granularity selected',
),
visibility: ({ controls }) =>
Boolean(controls?.rolling_type?.value) &&
controls.rolling_type.value !== RollingType.Cumsum,
visibility: ({ controls }, { name }) => {
// `rolling_type_b` refer to rolling_type in mixed timeseries Query B
const rollingTypeControlName = name.endsWith('_b')
? 'rolling_type_b'
: 'rolling_type';
return (
Boolean(controls[rollingTypeControlName]?.value) &&
controls[rollingTypeControlName]?.value !== RollingType.Cumsum
);
},
},
},
],
Expand All @@ -79,9 +86,16 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
'shown are the total of 7 periods. This will hide the "ramp up" ' +
'taking place over the first 7 periods',
),
visibility: ({ controls }) =>
Boolean(controls?.rolling_type?.value) &&
controls.rolling_type.value !== RollingType.Cumsum,
visibility: ({ controls }, { name }) => {
// `rolling_type_b` refer to rolling_type in mixed timeseries Query B
const rollingTypeControlName = name.endsWith('_b')
? 'rolling_type_b'
: 'rolling_type';
return (
Boolean(controls[rollingTypeControlName]?.value) &&
controls[rollingTypeControlName]?.value !== RollingType.Cumsum
);
},
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ export interface BaseControlConfig<
// TODO: add strict `chartState` typing (see superset-frontend/src/explore/types)
chartState?: AnyDict,
) => ExtraControlProps;
visibility?: (props: ControlPanelsContainerProps) => boolean;
visibility?: (
props: ControlPanelsContainerProps,
controlData: AnyDict,
) => boolean;
}

export interface ControlValueValidator<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,83 +21,65 @@ import {
QueryFormData,
QueryObject,
normalizeOrderBy,
PostProcessingPivot,
} from '@superset-ui/core';
import {
pivotOperator,
renameOperator,
flattenOperator,
isTimeComparison,
timeComparePivotOperator,
rollingWindowOperator,
timeCompareOperator,
resampleOperator,
} from '@superset-ui/chart-controls';
import {
retainFormDataSuffix,
removeFormDataSuffix,
} from '../utils/formDataSuffix';

export default function buildQuery(formData: QueryFormData) {
const {
adhoc_filters,
adhoc_filters_b,
groupby,
groupby_b,
limit,
limit_b,
timeseries_limit_metric,
timeseries_limit_metric_b,
metrics,
metrics_b,
order_desc,
order_desc_b,
...baseFormData
} = formData;
baseFormData.is_timeseries = true;
const formData1 = {
...baseFormData,
adhoc_filters,
columns: groupby,
limit,
timeseries_limit_metric,
metrics,
order_desc,
};
const formData2 = {
...baseFormData,
adhoc_filters: adhoc_filters_b,
columns: groupby_b,
limit: limit_b,
timeseries_limit_metric: timeseries_limit_metric_b,
metrics: metrics_b,
order_desc: order_desc_b,
const baseFormData = {
...formData,
is_timeseries: true,
columns: formData.groupby,
columns_b: formData.groupby_b,
};
const formData1 = removeFormDataSuffix(baseFormData, '_b');
const formData2 = retainFormDataSuffix(baseFormData, '_b');

const queryContexts = [formData1, formData2].map(fd =>
buildQueryContext(fd, baseQueryObject => {
const queryObject = {
...baseQueryObject,
is_timeseries: true,
};

const queryContextA = buildQueryContext(formData1, baseQueryObject => {
const queryObjectA = {
...baseQueryObject,
is_timeseries: true,
post_processing: [
pivotOperator(formData1, { ...baseQueryObject, is_timeseries: true }),
renameOperator(formData1, {
...baseQueryObject,
...{ is_timeseries: true },
}),
flattenOperator(formData1, baseQueryObject),
],
} as QueryObject;
return [normalizeOrderBy(queryObjectA)];
});
const pivotOperatorInRuntime: PostProcessingPivot = isTimeComparison(
fd,
queryObject,
)
? timeComparePivotOperator(fd, queryObject)
: pivotOperator(fd, queryObject);

const queryContextB = buildQueryContext(formData2, baseQueryObject => {
const queryObjectB = {
...baseQueryObject,
is_timeseries: true,
post_processing: [
pivotOperator(formData2, { ...baseQueryObject, is_timeseries: true }),
renameOperator(formData2, {
...baseQueryObject,
...{ is_timeseries: true },
}),
flattenOperator(formData2, baseQueryObject),
],
} as QueryObject;
return [normalizeOrderBy(queryObjectB)];
});
const tmpQueryObject = {
...queryObject,
time_offsets: isTimeComparison(fd, queryObject) ? fd.time_compare : [],
post_processing: [
pivotOperatorInRuntime,
rollingWindowOperator(fd, queryObject),
timeCompareOperator(fd, queryObject),
resampleOperator(fd, queryObject),
renameOperator(fd, queryObject),
flattenOperator(fd, queryObject),
],
} as QueryObject;
return [normalizeOrderBy(tmpQueryObject)];
}),
);

return {
...queryContextA,
queries: [...queryContextA.queries, ...queryContextB.queries],
...queryContexts[0],
queries: [...queryContexts[0].queries, ...queryContexts[1].queries],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
*/
import React from 'react';
import { t } from '@superset-ui/core';
import { cloneDeep } from 'lodash';
import {
ControlPanelConfig,
ControlPanelSectionConfig,
ControlSetRow,
CustomControlItem,
emitFilterControl,
sections,
sharedControls,
Expand Down Expand Up @@ -253,11 +255,33 @@ function createCustomizeSection(
];
}

function createAdvancedAnalyticsSection(
label: string,
controlSuffix: string,
): ControlPanelSectionConfig {
const aaWithSuffix = cloneDeep(sections.advancedAnalyticsControls);
aaWithSuffix.label = label;
if (!controlSuffix) {
return aaWithSuffix;
}
aaWithSuffix.controlSetRows.forEach(row =>
row.forEach((control: CustomControlItem) => {
if (control?.name) {
// eslint-disable-next-line no-param-reassign
control.name = `${control.name}${controlSuffix}`;
}
}),
);
return aaWithSuffix;
}

const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
createQuerySection(t('Query A'), ''),
createAdvancedAnalyticsSection(t('Advanced analytics Query A'), ''),
createQuerySection(t('Query B'), '_b'),
createAdvancedAnalyticsSection(t('Advanced analytics Query B'), '_b'),
{
label: t('Annotations and Layers'),
expanded: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* 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 { QueryFormData } from '@superset-ui/core';

export const retainFormDataSuffix = (
formData: QueryFormData,
controlSuffix: string,
): QueryFormData => {
/*
* retain controls by suffix and return a new formData
* eg:
* > const fd = { metrics: ['foo', 'bar'], metrics_b: ['zee'], limit: 100, ... }
* > removeFormDataSuffix(fd, '_b')
* { metrics: ['zee'], limit: 100, ... }
* */
const newFormData = {};

Object.entries(formData)
.sort(([a], [b]) => {
// items contained suffix before others
const weight_a = a.endsWith(controlSuffix) ? 1 : 0;
const weight_b = b.endsWith(controlSuffix) ? 1 : 0;
return weight_b - weight_a;
})
.forEach(([key, value]) => {
if (key.endsWith(controlSuffix)) {
newFormData[key.slice(0, -controlSuffix.length)] = value;
}

if (!key.endsWith(controlSuffix) && !(key in newFormData)) {
// ignore duplication
newFormData[key] = value;
}
});

return newFormData as QueryFormData;
};

export const removeFormDataSuffix = (
formData: QueryFormData,
controlSuffix: string,
): QueryFormData => {
/*
* remove unused controls by suffix and return a new formData
* eg:
* > const fd = { metrics: ['foo', 'bar'], metrics_b: ['zee'], limit: 100, ... }
* > removeUnusedFormData(fd, '_b')
* { metrics: ['foo', 'bar'], limit: 100, ... }
* */
const newFormData = {};
Object.entries(formData).forEach(([key, value]) => {
if (!key.endsWith(controlSuffix)) {
newFormData[key] = value;
}
});

return newFormData as QueryFormData;
};
Loading

0 comments on commit f157bc8

Please sign in to comment.