feat(plugin-chart-echarts): add support for generic axis to mixed chart (#20097)

* feat(plugin-chart-echarts): add support for generic axis to mixed chart

* fix tests + add new tests

* address review comments

* simplify control panel

* fix types and tests
This commit is contained in:
Ville Brofeldt 2022-05-19 13:51:52 +03:00 committed by GitHub
parent b2a7fadba9
commit d5c5e58583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 267 additions and 105 deletions

View File

@ -341,7 +341,7 @@ export interface ControlPanelSectionConfig {
} }
export interface ControlPanelConfig { export interface ControlPanelConfig {
controlPanelSections: ControlPanelSectionConfig[]; controlPanelSections: (ControlPanelSectionConfig | null)[];
controlOverrides?: ControlOverrides; controlOverrides?: ControlOverrides;
sectionOverrides?: SectionOverrides; sectionOverrides?: SectionOverrides;
onInit?: (state: ControlStateMapping) => void; onInit?: (state: ControlStateMapping) => void;
@ -413,3 +413,9 @@ export function isAdhocColumn(
): column is AdhocColumn { ): column is AdhocColumn {
return 'label' in column && 'sqlExpression' in column; return 'label' in column && 'sqlExpression' in column;
} }
export function isControlPanelSectionConfig(
section: ControlPanelSectionConfig | null,
): section is ControlPanelSectionConfig {
return section !== null;
}

View File

@ -18,10 +18,12 @@
*/ */
import { AdhocColumn } from '@superset-ui/core'; import { AdhocColumn } from '@superset-ui/core';
import { import {
ColumnMeta,
ControlPanelSectionConfig,
isAdhocColumn, isAdhocColumn,
isColumnMeta, isColumnMeta,
isControlPanelSectionConfig,
isSavedExpression, isSavedExpression,
ColumnMeta,
} from '../src'; } from '../src';
const ADHOC_COLUMN: AdhocColumn = { const ADHOC_COLUMN: AdhocColumn = {
@ -37,37 +39,46 @@ const SAVED_EXPRESSION: ColumnMeta = {
column_name: 'Saved expression', column_name: 'Saved expression',
expression: 'case when 1 = 1 then 1 else 2 end', expression: 'case when 1 = 1 then 1 else 2 end',
}; };
const CONTROL_PANEL_SECTION_CONFIG: ControlPanelSectionConfig = {
label: 'My Section',
description: 'My Description',
controlSetRows: [],
};
describe('isColumnMeta', () => { test('isColumnMeta returns false for AdhocColumn', () => {
it('returns false for AdhocColumn', () => { expect(isColumnMeta(ADHOC_COLUMN)).toEqual(false);
expect(isColumnMeta(ADHOC_COLUMN)).toEqual(false);
});
it('returns true for ColumnMeta', () => {
expect(isColumnMeta(COLUMN_META)).toEqual(true);
});
}); });
describe('isAdhocColumn', () => { test('isColumnMeta returns true for ColumnMeta', () => {
it('returns true for AdhocColumn', () => { expect(isColumnMeta(COLUMN_META)).toEqual(true);
expect(isAdhocColumn(ADHOC_COLUMN)).toEqual(true);
});
it('returns false for ColumnMeta', () => {
expect(isAdhocColumn(COLUMN_META)).toEqual(false);
});
}); });
describe('isSavedExpression', () => { test('isAdhocColumn returns true for AdhocColumn', () => {
it('returns false for AdhocColumn', () => { expect(isAdhocColumn(ADHOC_COLUMN)).toEqual(true);
expect(isSavedExpression(ADHOC_COLUMN)).toEqual(false); });
});
test('isAdhocColumn returns false for ColumnMeta', () => {
it('returns false for ColumnMeta without expression', () => { expect(isAdhocColumn(COLUMN_META)).toEqual(false);
expect(isSavedExpression(COLUMN_META)).toEqual(false); });
});
test('isSavedExpression returns false for AdhocColumn', () => {
it('returns true for ColumnMeta with expression', () => { expect(isSavedExpression(ADHOC_COLUMN)).toEqual(false);
expect(isSavedExpression(SAVED_EXPRESSION)).toEqual(true); });
});
test('isSavedExpression returns false for ColumnMeta without expression', () => {
expect(isSavedExpression(COLUMN_META)).toEqual(false);
});
test('isSavedExpression returns true for ColumnMeta with expression', () => {
expect(isSavedExpression(SAVED_EXPRESSION)).toEqual(true);
});
test('isControlPanelSectionConfig returns true for section', () => {
expect(isControlPanelSectionConfig(CONTROL_PANEL_SECTION_CONFIG)).toEqual(
true,
);
});
test('isControlPanelSectionConfig returns true for null value', () => {
expect(isControlPanelSectionConfig(null)).toEqual(false);
}); });

View File

@ -18,10 +18,12 @@
*/ */
import { import {
buildQueryContext, buildQueryContext,
QueryFormData, DTTM_ALIAS,
QueryObject, ensureIsArray,
normalizeOrderBy, normalizeOrderBy,
PostProcessingPivot, PostProcessingPivot,
QueryFormData,
QueryObject,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { import {
pivotOperator, pivotOperator,
@ -39,12 +41,13 @@ import {
} from '../utils/formDataSuffix'; } from '../utils/formDataSuffix';
export default function buildQuery(formData: QueryFormData) { export default function buildQuery(formData: QueryFormData) {
const { x_axis: index } = formData;
const is_timeseries = index === DTTM_ALIAS || !index;
const baseFormData = { const baseFormData = {
...formData, ...formData,
is_timeseries: true, is_timeseries,
columns: formData.groupby,
columns_b: formData.groupby_b,
}; };
const formData1 = removeFormDataSuffix(baseFormData, '_b'); const formData1 = removeFormDataSuffix(baseFormData, '_b');
const formData2 = retainFormDataSuffix(baseFormData, '_b'); const formData2 = retainFormDataSuffix(baseFormData, '_b');
@ -52,7 +55,9 @@ export default function buildQuery(formData: QueryFormData) {
buildQueryContext(fd, baseQueryObject => { buildQueryContext(fd, baseQueryObject => {
const queryObject = { const queryObject = {
...baseQueryObject, ...baseQueryObject,
is_timeseries: true, columns: [...ensureIsArray(index), ...ensureIsArray(fd.groupby)],
series_columns: fd.groupby,
is_timeseries,
}; };
const pivotOperatorInRuntime: PostProcessingPivot = isTimeComparison( const pivotOperatorInRuntime: PostProcessingPivot = isTimeComparison(
@ -60,7 +65,12 @@ export default function buildQuery(formData: QueryFormData) {
queryObject, queryObject,
) )
? timeComparePivotOperator(fd, queryObject) ? timeComparePivotOperator(fd, queryObject)
: pivotOperator(fd, queryObject); : pivotOperator(fd, {
...queryObject,
columns: fd.groupby,
index,
is_timeseries,
});
const tmpQueryObject = { const tmpQueryObject = {
...queryObject, ...queryObject,
@ -70,9 +80,13 @@ export default function buildQuery(formData: QueryFormData) {
rollingWindowOperator(fd, queryObject), rollingWindowOperator(fd, queryObject),
timeCompareOperator(fd, queryObject), timeCompareOperator(fd, queryObject),
resampleOperator(fd, queryObject), resampleOperator(fd, queryObject),
renameOperator(fd, queryObject), renameOperator(fd, {
...queryObject,
columns: fd.groupby,
is_timeseries,
}),
flattenOperator(fd, queryObject), flattenOperator(fd, queryObject),
], ].filter(Boolean),
} as QueryObject; } as QueryObject;
return [normalizeOrderBy(tmpQueryObject)]; return [normalizeOrderBy(tmpQueryObject)];
}), }),

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { t } from '@superset-ui/core'; import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { import {
ControlPanelConfig, ControlPanelConfig,
@ -31,7 +31,7 @@ import {
import { DEFAULT_FORM_DATA } from './types'; import { DEFAULT_FORM_DATA } from './types';
import { EchartsTimeseriesSeriesType } from '../Timeseries/types'; import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
import { legendSection, richTooltipSection } from '../controls'; import { legendSection, richTooltipSection, xAxisControl } from '../controls';
const { const {
area, area,
@ -278,6 +278,13 @@ function createAdvancedAnalyticsSection(
const config: ControlPanelConfig = { const config: ControlPanelConfig = {
controlPanelSections: [ controlPanelSections: [
sections.legacyTimeseriesTime, sections.legacyTimeseriesTime,
isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
? {
label: t('Shared query fields'),
expanded: true,
controlSetRows: [[xAxisControl]],
}
: null,
createQuerySection(t('Query A'), ''), createQuerySection(t('Query A'), ''),
createAdvancedAnalyticsSection(t('Advanced analytics Query A'), ''), createAdvancedAnalyticsSection(t('Advanced analytics Query A'), ''),
createQuerySection(t('Query B'), '_b'), createQuerySection(t('Query B'), '_b'),

View File

@ -17,19 +17,21 @@
* under the License. * under the License.
*/ */
import { import {
t,
ChartMetadata,
ChartPlugin,
AnnotationType, AnnotationType,
Behavior, Behavior,
ChartMetadata,
ChartPlugin,
FeatureFlag,
isFeatureEnabled,
t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import buildQuery from './buildQuery'; import buildQuery from './buildQuery';
import controlPanel from './controlPanel'; import controlPanel from './controlPanel';
import transformProps from './transformProps'; import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png'; import thumbnail from './images/thumbnail.png';
import { import {
EchartsMixedTimeseriesProps,
EchartsMixedTimeseriesFormData, EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps,
} from './types'; } from './types';
export default class EchartsTimeseriesChartPlugin extends ChartPlugin< export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
@ -55,16 +57,22 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
behaviors: [Behavior.INTERACTIVE_CHART], behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'), category: t('Evolution'),
credits: ['https://echarts.apache.org'], credits: ['https://echarts.apache.org'],
description: t( description: isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
'Visualize two different time series using the same x-axis time range. Note that each time series can be visualized differently (e.g. 1 using bars and 1 using a line).', ? t(
), 'Visualize two different series using the same x-axis. Note that both series can be visualized with a different chart type (e.g. 1 using bars and 1 using a line).',
)
: t(
'Visualize two different time series using the same x-axis. Note that each time series can be visualized differently (e.g. 1 using bars and 1 using a line).',
),
supportedAnnotationTypes: [ supportedAnnotationTypes: [
AnnotationType.Event, AnnotationType.Event,
AnnotationType.Formula, AnnotationType.Formula,
AnnotationType.Interval, AnnotationType.Interval,
AnnotationType.Timeseries, AnnotationType.Timeseries,
], ],
name: t('Mixed Time-Series'), name: isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
? t('Mixed Chart')
: t('Mixed Time-Series'),
thumbnail, thumbnail,
tags: [ tags: [
t('Advanced-Analytics'), t('Advanced-Analytics'),
@ -73,7 +81,6 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
t('Experimental'), t('Experimental'),
t('Line'), t('Line'),
t('Multi-Variables'), t('Multi-Variables'),
t('Predictive'),
t('Time'), t('Time'),
t('Transformable'), t('Transformable'),
], ],

View File

@ -21,12 +21,15 @@ import {
AnnotationLayer, AnnotationLayer,
CategoricalColorNamespace, CategoricalColorNamespace,
DataRecordValue, DataRecordValue,
TimeseriesDataRecord, DTTM_ALIAS,
GenericDataType,
getColumnLabel,
getNumberFormatter, getNumberFormatter,
isEventAnnotationLayer, isEventAnnotationLayer,
isFormulaAnnotationLayer, isFormulaAnnotationLayer,
isIntervalAnnotationLayer, isIntervalAnnotationLayer,
isTimeseriesAnnotationLayer, isTimeseriesAnnotationLayer,
TimeseriesDataRecord,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { EChartsCoreOption, SeriesOption } from 'echarts'; import { EChartsCoreOption, SeriesOption } from 'echarts';
import { import {
@ -41,6 +44,8 @@ import {
currentSeries, currentSeries,
dedupSeries, dedupSeries,
extractSeries, extractSeries,
getAxisType,
getColtypesMapping,
getLegendProps, getLegendProps,
} from '../utils/series'; } from '../utils/series';
import { extractAnnotationLabels } from '../utils/annotation'; import { extractAnnotationLabels } from '../utils/annotation';
@ -62,7 +67,7 @@ import {
transformSeries, transformSeries,
transformTimeseriesAnnotation, transformTimeseriesAnnotation,
} from '../Timeseries/transformers'; } from '../Timeseries/transformers';
import { TIMESERIES_CONSTANTS } from '../constants'; import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
export default function transformProps( export default function transformProps(
chartProps: EchartsMixedTimeseriesProps, chartProps: EchartsMixedTimeseriesProps,
@ -124,24 +129,35 @@ export default function transformProps(
groupbyB, groupbyB,
emitFilter, emitFilter,
emitFilterB, emitFilterB,
xAxis: xAxisOrig,
xAxisTitle, xAxisTitle,
yAxisTitle, yAxisTitle,
xAxisTitleMargin, xAxisTitleMargin,
yAxisTitleMargin, yAxisTitleMargin,
yAxisTitlePosition, yAxisTitlePosition,
sliceId, sliceId,
timeGrainSqla,
}: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const xAxisCol =
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
const rebasedDataA = rebaseForecastDatum(data1, verboseMap); const rebasedDataA = rebaseForecastDatum(data1, verboseMap);
const rawSeriesA = extractSeries(rebasedDataA, { const rawSeriesA = extractSeries(rebasedDataA, {
fillNeighborValue: stack ? 0 : undefined, fillNeighborValue: stack ? 0 : undefined,
xAxis: xAxisCol,
}); });
const rebasedDataB = rebaseForecastDatum(data2, verboseMap); const rebasedDataB = rebaseForecastDatum(data2, verboseMap);
const rawSeriesB = extractSeries(rebasedDataB, { const rawSeriesB = extractSeries(rebasedDataB, {
fillNeighborValue: stackB ? 0 : undefined, fillNeighborValue: stackB ? 0 : undefined,
xAxis: xAxisCol,
}); });
const dataTypes = getColtypesMapping(queriesData[0]);
const xAxisDataType = dataTypes?.[xAxisCol];
const xAxisType = getAxisType(xAxisDataType);
const series: SeriesOption[] = []; const series: SeriesOption[] = [];
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat); const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
const formatterSecondary = getNumberFormatter( const formatterSecondary = getNumberFormatter(
@ -255,8 +271,14 @@ export default function transformProps(
if (max === undefined) max = 1; if (max === undefined) max = 1;
} }
const tooltipTimeFormatter = getTooltipTimeFormatter(tooltipTimeFormat); const tooltipFormatter =
const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat); xAxisDataType === GenericDataType.TEMPORAL
? getTooltipTimeFormatter(tooltipTimeFormat)
: String;
const xAxisFormatter =
xAxisDataType === GenericDataType.TEMPORAL
? getXAxisFormatter(xAxisTimeFormat)
: String;
const addYAxisTitleOffset = !!(yAxisTitle || yAxisTitleSecondary); const addYAxisTitleOffset = !!(yAxisTitle || yAxisTitleSecondary);
const addXAxisTitleOffset = !!xAxisTitle; const addXAxisTitleOffset = !!xAxisTitle;
@ -298,7 +320,7 @@ export default function transformProps(
...chartPadding, ...chartPadding,
}, },
xAxis: { xAxis: {
type: 'time', type: xAxisType,
name: xAxisTitle, name: xAxisTitle,
nameGap: convertInteger(xAxisTitleMargin), nameGap: convertInteger(xAxisTitleMargin),
nameLocation: 'middle', nameLocation: 'middle',
@ -306,6 +328,10 @@ export default function transformProps(
formatter: xAxisFormatter, formatter: xAxisFormatter,
rotate: xAxisLabelRotation, rotate: xAxisLabelRotation,
}, },
minInterval:
xAxisType === 'time' && timeGrainSqla
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
: 0,
}, },
yAxis: [ yAxis: [
{ {
@ -350,7 +376,7 @@ export default function transformProps(
forecastValue.sort((a, b) => b.data[1] - a.data[1]); forecastValue.sort((a, b) => b.data[1] - a.data[1]);
} }
const rows: Array<string> = [`${tooltipTimeFormatter(xValue)}`]; const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
const forecastValues = const forecastValues =
extractForecastValuesFromTooltipParams(forecastValue); extractForecastValuesFromTooltipParams(forecastValue);

View File

@ -20,9 +20,9 @@ import {
buildQueryContext, buildQueryContext,
DTTM_ALIAS, DTTM_ALIAS,
ensureIsArray, ensureIsArray,
QueryFormData,
normalizeOrderBy, normalizeOrderBy,
PostProcessingPivot, PostProcessingPivot,
QueryFormData,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { import {
rollingWindowOperator, rollingWindowOperator,
@ -94,13 +94,13 @@ export default function buildQuery(formData: QueryFormData) {
resampleOperator(formData, baseQueryObject), resampleOperator(formData, baseQueryObject),
renameOperator(formData, { renameOperator(formData, {
...baseQueryObject, ...baseQueryObject,
...{ is_timeseries }, is_timeseries,
}), }),
contributionOperator(formData, baseQueryObject), contributionOperator(formData, baseQueryObject),
flattenOperator(formData, baseQueryObject), flattenOperator(formData, baseQueryObject),
// todo: move prophet before flatten // todo: move prophet before flatten
prophetOperator(formData, baseQueryObject), prophetOperator(formData, baseQueryObject),
].filter(op => op), ].filter(Boolean),
}, },
]; ];
}); });

View File

@ -29,7 +29,6 @@ import {
isFormulaAnnotationLayer, isFormulaAnnotationLayer,
isIntervalAnnotationLayer, isIntervalAnnotationLayer,
isTimeseriesAnnotationLayer, isTimeseriesAnnotationLayer,
TimeGranularity,
TimeseriesChartDataResponseResult, TimeseriesChartDataResponseResult,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { EChartsCoreOption, SeriesOption } from 'echarts'; import { EChartsCoreOption, SeriesOption } from 'echarts';
@ -47,6 +46,7 @@ import {
currentSeries, currentSeries,
dedupSeries, dedupSeries,
extractSeries, extractSeries,
getAxisType,
getColtypesMapping, getColtypesMapping,
getLegendProps, getLegendProps,
} from '../utils/series'; } from '../utils/series';
@ -70,15 +70,7 @@ import {
transformSeries, transformSeries,
transformTimeseriesAnnotation, transformTimeseriesAnnotation,
} from './transformers'; } from './transformers';
import { TIMESERIES_CONSTANTS } from '../constants'; import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
const TimeGrainToTimestamp = {
[TimeGranularity.HOUR]: 3600 * 1000,
[TimeGranularity.DAY]: 3600 * 1000 * 24,
[TimeGranularity.MONTH]: 3600 * 1000 * 24 * 31,
[TimeGranularity.QUARTER]: 3600 * 1000 * 24 * 31 * 3,
[TimeGranularity.YEAR]: 3600 * 1000 * 24 * 31 * 12,
};
export default function transformProps( export default function transformProps(
chartProps: EchartsTimeseriesChartProps, chartProps: EchartsTimeseriesChartProps,
@ -157,18 +149,7 @@ export default function transformProps(
Object.values(rawSeries).map(series => series.name as string), Object.values(rawSeries).map(series => series.name as string),
); );
const xAxisDataType = dataTypes?.[xAxisCol]; const xAxisDataType = dataTypes?.[xAxisCol];
let xAxisType: 'time' | 'value' | 'category'; const xAxisType = getAxisType(xAxisDataType);
switch (xAxisDataType) {
case GenericDataType.TEMPORAL:
xAxisType = 'time';
break;
case GenericDataType.NUMERIC:
xAxisType = 'value';
break;
default:
xAxisType = 'category';
break;
}
const series: SeriesOption[] = []; const series: SeriesOption[] = [];
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat); const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
@ -342,7 +323,7 @@ export default function transformProps(
}, },
minInterval: minInterval:
xAxisType === 'time' && timeGrainSqla xAxisType === 'time' && timeGrainSqla
? TimeGrainToTimestamp[timeGrainSqla] ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
: 0, : 0,
}; };
let yAxis: any = { let yAxis: any = {

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { TimeGranularity } from '@superset-ui/core';
import { LabelPositionEnum } from './types'; import { LabelPositionEnum } from './types';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
@ -59,3 +60,11 @@ export enum OpacityEnum {
SemiTransparent = 0.3, SemiTransparent = 0.3,
NonTransparent = 1, NonTransparent = 1,
} }
export const TIMEGRAIN_TO_TIMESTAMP = {
[TimeGranularity.HOUR]: 3600 * 1000,
[TimeGranularity.DAY]: 3600 * 1000 * 24,
[TimeGranularity.MONTH]: 3600 * 1000 * 24 * 31,
[TimeGranularity.QUARTER]: 3600 * 1000 * 24 * 31 * 3,
[TimeGranularity.YEAR]: 3600 * 1000 * 24 * 31 * 12,
};

View File

@ -239,3 +239,15 @@ export const currentSeries = {
name: '', name: '',
legend: '', legend: '',
}; };
export function getAxisType(
dataType?: GenericDataType,
): 'time' | 'value' | 'category' {
if (dataType === GenericDataType.TEMPORAL) {
return 'time';
}
if (dataType === GenericDataType.NUMERIC) {
return 'value';
}
return 'category';
}

View File

@ -84,8 +84,8 @@ const formDataMixedChartWithAA = {
}; };
test('should compile query object A', () => { test('should compile query object A', () => {
const query_a = buildQuery(formDataMixedChart).queries[0]; const query = buildQuery(formDataMixedChart).queries[0];
expect(query_a).toEqual({ expect(query).toEqual({
time_range: '1980 : 2000', time_range: '1980 : 2000',
since: undefined, since: undefined,
until: undefined, until: undefined,
@ -103,7 +103,7 @@ test('should compile query object A', () => {
annotation_layers: [], annotation_layers: [],
row_limit: 10, row_limit: 10,
row_offset: undefined, row_offset: undefined,
series_columns: undefined, series_columns: ['foo'],
series_limit: undefined, series_limit: undefined,
series_limit_metric: undefined, series_limit_metric: undefined,
timeseries_limit: 5, timeseries_limit: 5,
@ -128,9 +128,6 @@ test('should compile query object A', () => {
reset_index: false, reset_index: false,
}, },
}, },
undefined,
undefined,
undefined,
{ {
operation: 'rename', operation: 'rename',
options: { options: {
@ -150,8 +147,8 @@ test('should compile query object A', () => {
}); });
test('should compile query object B', () => { test('should compile query object B', () => {
const query_a = buildQuery(formDataMixedChart).queries[1]; const query = buildQuery(formDataMixedChart).queries[1];
expect(query_a).toEqual({ expect(query).toEqual({
time_range: '1980 : 2000', time_range: '1980 : 2000',
since: undefined, since: undefined,
until: undefined, until: undefined,
@ -169,7 +166,7 @@ test('should compile query object B', () => {
annotation_layers: [], annotation_layers: [],
row_limit: 100, row_limit: 100,
row_offset: undefined, row_offset: undefined,
series_columns: undefined, series_columns: [],
series_limit: undefined, series_limit: undefined,
series_limit_metric: undefined, series_limit_metric: undefined,
timeseries_limit: 0, timeseries_limit: 0,
@ -194,10 +191,6 @@ test('should compile query object B', () => {
reset_index: false, reset_index: false,
}, },
}, },
undefined,
undefined,
undefined,
undefined,
{ {
operation: 'flatten', operation: 'flatten',
}, },
@ -207,14 +200,14 @@ test('should compile query object B', () => {
}); });
test('should compile AA in query A', () => { test('should compile AA in query A', () => {
const query_a = buildQuery(formDataMixedChartWithAA).queries[0]; const query = buildQuery(formDataMixedChartWithAA).queries[0];
// time comparison // time comparison
expect(query_a?.time_offsets).toEqual(['1 years ago']); expect(query.time_offsets).toEqual(['1 years ago']);
// cumsum // cumsum
expect( expect(
// prettier-ignore // prettier-ignore
query_a query
.post_processing .post_processing
?.find(operator => operator?.operation === 'cum') ?.find(operator => operator?.operation === 'cum')
?.operation, ?.operation,
@ -223,7 +216,7 @@ test('should compile AA in query A', () => {
// resample // resample
expect( expect(
// prettier-ignore // prettier-ignore
query_a query
.post_processing .post_processing
?.find(operator => operator?.operation === 'resample'), ?.find(operator => operator?.operation === 'resample'),
).toEqual({ ).toEqual({
@ -237,14 +230,14 @@ test('should compile AA in query A', () => {
}); });
test('should compile AA in query B', () => { test('should compile AA in query B', () => {
const query_b = buildQuery(formDataMixedChartWithAA).queries[1]; const query = buildQuery(formDataMixedChartWithAA).queries[1];
// time comparison // time comparison
expect(query_b?.time_offsets).toEqual(['3 years ago']); expect(query.time_offsets).toEqual(['3 years ago']);
// rolling total // rolling total
expect( expect(
// prettier-ignore // prettier-ignore
query_b query
.post_processing .post_processing
?.find(operator => operator?.operation === 'rolling'), ?.find(operator => operator?.operation === 'rolling'),
).toEqual({ ).toEqual({
@ -263,7 +256,7 @@ test('should compile AA in query B', () => {
// resample // resample
expect( expect(
// prettier-ignore // prettier-ignore
query_b query
.post_processing .post_processing
?.find(operator => operator?.operation === 'resample'), ?.find(operator => operator?.operation === 'resample'),
).toEqual({ ).toEqual({
@ -275,3 +268,99 @@ test('should compile AA in query B', () => {
}, },
}); });
}); });
test('should compile query objects with x-axis', () => {
const { queries } = buildQuery({
...formDataMixedChart,
x_axis: 'my_index',
});
expect(queries[0]).toEqual({
time_range: '1980 : 2000',
since: undefined,
until: undefined,
granularity: 'ds',
filters: [],
extras: {
having: '',
having_druid: [],
time_grain_sqla: 'P1W',
where: "(foo in ('a', 'b'))",
},
applied_time_extras: {},
columns: ['my_index', 'foo'],
metrics: ['sum(sales)'],
annotation_layers: [],
row_limit: 10,
row_offset: undefined,
series_columns: ['foo'],
series_limit: undefined,
series_limit_metric: undefined,
timeseries_limit: 5,
url_params: {},
custom_params: {},
custom_form_data: {},
is_timeseries: false,
time_offsets: [],
post_processing: [
{
operation: 'pivot',
options: {
aggregates: {
'sum(sales)': {
operator: 'mean',
},
},
columns: ['foo'],
drop_missing_columns: false,
flatten_columns: false,
index: ['my_index'],
reset_index: false,
},
},
{
operation: 'rename',
options: {
columns: {
'sum(sales)': null,
},
inplace: true,
level: 0,
},
},
{
operation: 'flatten',
},
],
orderby: [['count', false]],
});
// check the main props on the second query
expect(queries[1]).toEqual(
expect.objectContaining({
is_timeseries: false,
columns: ['my_index'],
series_columns: [],
metrics: ['count'],
post_processing: [
{
operation: 'pivot',
options: {
aggregates: {
count: {
operator: 'mean',
},
},
columns: [],
drop_missing_columns: false,
flatten_columns: false,
index: ['my_index'],
reset_index: false,
},
},
{
operation: 'flatten',
},
],
}),
);
});

View File

@ -24,6 +24,7 @@ import {
import { import {
ControlPanelConfig, ControlPanelConfig,
expandControlConfig, expandControlConfig,
isControlPanelSectionConfig,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
import * as SECTIONS from 'src/explore/controlPanels/sections'; import * as SECTIONS from 'src/explore/controlPanels/sections';
@ -60,8 +61,7 @@ const getMemoizedSectionsToRender = memoizeOne(
: ['granularity_sqla', 'time_grain_sqla']; : ['granularity_sqla', 'time_grain_sqla'];
return [datasourceAndVizType] return [datasourceAndVizType]
.concat(controlPanelSections) .concat(controlPanelSections.filter(isControlPanelSectionConfig))
.filter(section => !!section)
.map(section => { .map(section => {
const { controlSetRows } = section; const { controlSetRows } = section;
return { return {