mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
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:
@@ -18,10 +18,12 @@
|
||||
*/
|
||||
import {
|
||||
buildQueryContext,
|
||||
QueryFormData,
|
||||
QueryObject,
|
||||
DTTM_ALIAS,
|
||||
ensureIsArray,
|
||||
normalizeOrderBy,
|
||||
PostProcessingPivot,
|
||||
QueryFormData,
|
||||
QueryObject,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
pivotOperator,
|
||||
@@ -39,12 +41,13 @@ import {
|
||||
} from '../utils/formDataSuffix';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { x_axis: index } = formData;
|
||||
const is_timeseries = index === DTTM_ALIAS || !index;
|
||||
const baseFormData = {
|
||||
...formData,
|
||||
is_timeseries: true,
|
||||
columns: formData.groupby,
|
||||
columns_b: formData.groupby_b,
|
||||
is_timeseries,
|
||||
};
|
||||
|
||||
const formData1 = removeFormDataSuffix(baseFormData, '_b');
|
||||
const formData2 = retainFormDataSuffix(baseFormData, '_b');
|
||||
|
||||
@@ -52,7 +55,9 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
buildQueryContext(fd, baseQueryObject => {
|
||||
const queryObject = {
|
||||
...baseQueryObject,
|
||||
is_timeseries: true,
|
||||
columns: [...ensureIsArray(index), ...ensureIsArray(fd.groupby)],
|
||||
series_columns: fd.groupby,
|
||||
is_timeseries,
|
||||
};
|
||||
|
||||
const pivotOperatorInRuntime: PostProcessingPivot = isTimeComparison(
|
||||
@@ -60,7 +65,12 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
queryObject,
|
||||
)
|
||||
? timeComparePivotOperator(fd, queryObject)
|
||||
: pivotOperator(fd, queryObject);
|
||||
: pivotOperator(fd, {
|
||||
...queryObject,
|
||||
columns: fd.groupby,
|
||||
index,
|
||||
is_timeseries,
|
||||
});
|
||||
|
||||
const tmpQueryObject = {
|
||||
...queryObject,
|
||||
@@ -70,9 +80,13 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
rollingWindowOperator(fd, queryObject),
|
||||
timeCompareOperator(fd, queryObject),
|
||||
resampleOperator(fd, queryObject),
|
||||
renameOperator(fd, queryObject),
|
||||
renameOperator(fd, {
|
||||
...queryObject,
|
||||
columns: fd.groupby,
|
||||
is_timeseries,
|
||||
}),
|
||||
flattenOperator(fd, queryObject),
|
||||
],
|
||||
].filter(Boolean),
|
||||
} as QueryObject;
|
||||
return [normalizeOrderBy(tmpQueryObject)];
|
||||
}),
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
|
||||
import { legendSection, richTooltipSection } from '../controls';
|
||||
import { legendSection, richTooltipSection, xAxisControl } from '../controls';
|
||||
|
||||
const {
|
||||
area,
|
||||
@@ -278,6 +278,13 @@ function createAdvancedAnalyticsSection(
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
sections.legacyTimeseriesTime,
|
||||
isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
|
||||
? {
|
||||
label: t('Shared query fields'),
|
||||
expanded: true,
|
||||
controlSetRows: [[xAxisControl]],
|
||||
}
|
||||
: null,
|
||||
createQuerySection(t('Query A'), ''),
|
||||
createAdvancedAnalyticsSection(t('Advanced analytics Query A'), ''),
|
||||
createQuerySection(t('Query B'), '_b'),
|
||||
|
||||
@@ -17,19 +17,21 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
t,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
AnnotationType,
|
||||
Behavior,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import {
|
||||
EchartsMixedTimeseriesProps,
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps,
|
||||
} from './types';
|
||||
|
||||
export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
||||
@@ -55,16 +57,22 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
||||
category: t('Evolution'),
|
||||
credits: ['https://echarts.apache.org'],
|
||||
description: t(
|
||||
'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).',
|
||||
),
|
||||
description: isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
|
||||
? 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: [
|
||||
AnnotationType.Event,
|
||||
AnnotationType.Formula,
|
||||
AnnotationType.Interval,
|
||||
AnnotationType.Timeseries,
|
||||
],
|
||||
name: t('Mixed Time-Series'),
|
||||
name: isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES)
|
||||
? t('Mixed Chart')
|
||||
: t('Mixed Time-Series'),
|
||||
thumbnail,
|
||||
tags: [
|
||||
t('Advanced-Analytics'),
|
||||
@@ -73,7 +81,6 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
||||
t('Experimental'),
|
||||
t('Line'),
|
||||
t('Multi-Variables'),
|
||||
t('Predictive'),
|
||||
t('Time'),
|
||||
t('Transformable'),
|
||||
],
|
||||
|
||||
@@ -21,12 +21,15 @@ import {
|
||||
AnnotationLayer,
|
||||
CategoricalColorNamespace,
|
||||
DataRecordValue,
|
||||
TimeseriesDataRecord,
|
||||
DTTM_ALIAS,
|
||||
GenericDataType,
|
||||
getColumnLabel,
|
||||
getNumberFormatter,
|
||||
isEventAnnotationLayer,
|
||||
isFormulaAnnotationLayer,
|
||||
isIntervalAnnotationLayer,
|
||||
isTimeseriesAnnotationLayer,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import { EChartsCoreOption, SeriesOption } from 'echarts';
|
||||
import {
|
||||
@@ -41,6 +44,8 @@ import {
|
||||
currentSeries,
|
||||
dedupSeries,
|
||||
extractSeries,
|
||||
getAxisType,
|
||||
getColtypesMapping,
|
||||
getLegendProps,
|
||||
} from '../utils/series';
|
||||
import { extractAnnotationLabels } from '../utils/annotation';
|
||||
@@ -62,7 +67,7 @@ import {
|
||||
transformSeries,
|
||||
transformTimeseriesAnnotation,
|
||||
} from '../Timeseries/transformers';
|
||||
import { TIMESERIES_CONSTANTS } from '../constants';
|
||||
import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
|
||||
|
||||
export default function transformProps(
|
||||
chartProps: EchartsMixedTimeseriesProps,
|
||||
@@ -124,24 +129,35 @@ export default function transformProps(
|
||||
groupbyB,
|
||||
emitFilter,
|
||||
emitFilterB,
|
||||
xAxis: xAxisOrig,
|
||||
xAxisTitle,
|
||||
yAxisTitle,
|
||||
xAxisTitleMargin,
|
||||
yAxisTitleMargin,
|
||||
yAxisTitlePosition,
|
||||
sliceId,
|
||||
timeGrainSqla,
|
||||
}: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
|
||||
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
|
||||
const xAxisCol =
|
||||
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
|
||||
|
||||
const rebasedDataA = rebaseForecastDatum(data1, verboseMap);
|
||||
const rawSeriesA = extractSeries(rebasedDataA, {
|
||||
fillNeighborValue: stack ? 0 : undefined,
|
||||
xAxis: xAxisCol,
|
||||
});
|
||||
const rebasedDataB = rebaseForecastDatum(data2, verboseMap);
|
||||
const rawSeriesB = extractSeries(rebasedDataB, {
|
||||
fillNeighborValue: stackB ? 0 : undefined,
|
||||
xAxis: xAxisCol,
|
||||
});
|
||||
|
||||
const dataTypes = getColtypesMapping(queriesData[0]);
|
||||
const xAxisDataType = dataTypes?.[xAxisCol];
|
||||
const xAxisType = getAxisType(xAxisDataType);
|
||||
const series: SeriesOption[] = [];
|
||||
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
|
||||
const formatterSecondary = getNumberFormatter(
|
||||
@@ -255,8 +271,14 @@ export default function transformProps(
|
||||
if (max === undefined) max = 1;
|
||||
}
|
||||
|
||||
const tooltipTimeFormatter = getTooltipTimeFormatter(tooltipTimeFormat);
|
||||
const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat);
|
||||
const tooltipFormatter =
|
||||
xAxisDataType === GenericDataType.TEMPORAL
|
||||
? getTooltipTimeFormatter(tooltipTimeFormat)
|
||||
: String;
|
||||
const xAxisFormatter =
|
||||
xAxisDataType === GenericDataType.TEMPORAL
|
||||
? getXAxisFormatter(xAxisTimeFormat)
|
||||
: String;
|
||||
|
||||
const addYAxisTitleOffset = !!(yAxisTitle || yAxisTitleSecondary);
|
||||
const addXAxisTitleOffset = !!xAxisTitle;
|
||||
@@ -298,7 +320,7 @@ export default function transformProps(
|
||||
...chartPadding,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
type: xAxisType,
|
||||
name: xAxisTitle,
|
||||
nameGap: convertInteger(xAxisTitleMargin),
|
||||
nameLocation: 'middle',
|
||||
@@ -306,6 +328,10 @@ export default function transformProps(
|
||||
formatter: xAxisFormatter,
|
||||
rotate: xAxisLabelRotation,
|
||||
},
|
||||
minInterval:
|
||||
xAxisType === 'time' && timeGrainSqla
|
||||
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
|
||||
: 0,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
@@ -350,7 +376,7 @@ export default function transformProps(
|
||||
forecastValue.sort((a, b) => b.data[1] - a.data[1]);
|
||||
}
|
||||
|
||||
const rows: Array<string> = [`${tooltipTimeFormatter(xValue)}`];
|
||||
const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
|
||||
const forecastValues =
|
||||
extractForecastValuesFromTooltipParams(forecastValue);
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
buildQueryContext,
|
||||
DTTM_ALIAS,
|
||||
ensureIsArray,
|
||||
QueryFormData,
|
||||
normalizeOrderBy,
|
||||
PostProcessingPivot,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
rollingWindowOperator,
|
||||
@@ -94,13 +94,13 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
resampleOperator(formData, baseQueryObject),
|
||||
renameOperator(formData, {
|
||||
...baseQueryObject,
|
||||
...{ is_timeseries },
|
||||
is_timeseries,
|
||||
}),
|
||||
contributionOperator(formData, baseQueryObject),
|
||||
flattenOperator(formData, baseQueryObject),
|
||||
// todo: move prophet before flatten
|
||||
prophetOperator(formData, baseQueryObject),
|
||||
].filter(op => op),
|
||||
].filter(Boolean),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
isFormulaAnnotationLayer,
|
||||
isIntervalAnnotationLayer,
|
||||
isTimeseriesAnnotationLayer,
|
||||
TimeGranularity,
|
||||
TimeseriesChartDataResponseResult,
|
||||
} from '@superset-ui/core';
|
||||
import { EChartsCoreOption, SeriesOption } from 'echarts';
|
||||
@@ -47,6 +46,7 @@ import {
|
||||
currentSeries,
|
||||
dedupSeries,
|
||||
extractSeries,
|
||||
getAxisType,
|
||||
getColtypesMapping,
|
||||
getLegendProps,
|
||||
} from '../utils/series';
|
||||
@@ -70,15 +70,7 @@ import {
|
||||
transformSeries,
|
||||
transformTimeseriesAnnotation,
|
||||
} from './transformers';
|
||||
import { TIMESERIES_CONSTANTS } 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,
|
||||
};
|
||||
import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
|
||||
|
||||
export default function transformProps(
|
||||
chartProps: EchartsTimeseriesChartProps,
|
||||
@@ -157,18 +149,7 @@ export default function transformProps(
|
||||
Object.values(rawSeries).map(series => series.name as string),
|
||||
);
|
||||
const xAxisDataType = dataTypes?.[xAxisCol];
|
||||
let xAxisType: 'time' | 'value' | 'category';
|
||||
switch (xAxisDataType) {
|
||||
case GenericDataType.TEMPORAL:
|
||||
xAxisType = 'time';
|
||||
break;
|
||||
case GenericDataType.NUMERIC:
|
||||
xAxisType = 'value';
|
||||
break;
|
||||
default:
|
||||
xAxisType = 'category';
|
||||
break;
|
||||
}
|
||||
const xAxisType = getAxisType(xAxisDataType);
|
||||
const series: SeriesOption[] = [];
|
||||
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
|
||||
|
||||
@@ -342,7 +323,7 @@ export default function transformProps(
|
||||
},
|
||||
minInterval:
|
||||
xAxisType === 'time' && timeGrainSqla
|
||||
? TimeGrainToTimestamp[timeGrainSqla]
|
||||
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
|
||||
: 0,
|
||||
};
|
||||
let yAxis: any = {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeGranularity } from '@superset-ui/core';
|
||||
import { LabelPositionEnum } from './types';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
@@ -59,3 +60,11 @@ export enum OpacityEnum {
|
||||
SemiTransparent = 0.3,
|
||||
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: '',
|
||||
legend: '',
|
||||
};
|
||||
|
||||
export function getAxisType(
|
||||
dataType?: GenericDataType,
|
||||
): 'time' | 'value' | 'category' {
|
||||
if (dataType === GenericDataType.TEMPORAL) {
|
||||
return 'time';
|
||||
}
|
||||
if (dataType === GenericDataType.NUMERIC) {
|
||||
return 'value';
|
||||
}
|
||||
return 'category';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user