mirror of https://github.com/apache/superset.git
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:
parent
b2a7fadba9
commit
d5c5e58583
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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)];
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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'),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue