Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(echarts): Implement stream graph for Echarts Timeseries #23410

Merged
merged 13 commits into from
Mar 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ import {
showValueControl,
richTooltipSection,
seriesOrderSection,
percentageThresholdControl,
} from '../../controls';
import { AreaChartExtraControlsOptions } from '../../constants';
import { AreaChartStackControlOptions } from '../../constants';

const {
logAxis,
Expand Down Expand Up @@ -109,13 +110,14 @@ const config: ControlPanelConfig = {
type: 'SelectControl',
label: t('Stacked Style'),
renderTrigger: true,
choices: AreaChartExtraControlsOptions,
choices: AreaChartStackControlOptions,
default: null,
description: t('Stack series on top of each other'),
},
},
],
[onlyTotalControl],
[percentageThresholdControl],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice bycatch

[
{
name: 'show_extra_controls',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ import {
sections,
sharedControls,
} from '@superset-ui/chart-controls';

import { OrientationType } from '../../types';
import {
DEFAULT_FORM_DATA,
TIME_SERIES_DESCRIPTION_TEXT,
} from '../../constants';
import {
legendSection,
richTooltipSection,
seriesOrderSection,
showValueSection,
} from '../../../controls';

import { OrientationType } from '../../types';
import {
DEFAULT_FORM_DATA,
TIME_SERIES_DESCRIPTION_TEXT,
} from '../../constants';

const {
logAxis,
minorSplitLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import { ZRLineType } from 'echarts/types/src/util/types';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
TimeseriesChartTransformedProps,
OrientationType,
} from './types';
Expand Down Expand Up @@ -74,6 +73,7 @@ import {
import { convertInteger } from '../utils/convertInteger';
import { defaultGrid, defaultYAxis } from '../defaults';
import {
getBaselineSeriesForStream,
getPadding,
getTooltipTimeFormatter,
getXAxisFormatter,
Expand All @@ -84,7 +84,7 @@ import {
transformTimeseriesAnnotation,
} from './transformers';
import {
AreaChartExtraControlsValue,
StackControlsValue,
TIMESERIES_CONSTANTS,
TIMEGRAIN_TO_TIMESTAMP,
} from '../constants';
Expand Down Expand Up @@ -195,7 +195,6 @@ export default function transformProps(
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
xAxis: xAxisLabel,
extraMetricLabels,
removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
stack,
totalStackedValues,
isHorizontal,
Expand All @@ -210,7 +209,7 @@ export default function transformProps(
const seriesContexts = extractForecastSeriesContexts(
Object.values(rawSeries).map(series => series.name as string),
);
const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
const isAreaExpand = stack === StackControlsValue.Expand;
const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];

const xAxisType = getAxisType(xAxisDataType);
Expand Down Expand Up @@ -243,9 +242,29 @@ export default function transformProps(
isHorizontal,
lineStyle,
});
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
if (stack === StackControlsValue.Stream) {
// bug in Echarts - `stackStrategy: 'all'` doesn't work with nulls, so we cast them to 0
series.push({
...transformedSeries,
data: (transformedSeries.data as any).map(
(row: [string | number, number]) => [row[0], row[1] ?? 0],
),
});
} else {
series.push(transformedSeries);
}
}
});

if (stack === StackControlsValue.Stream) {
const baselineSeries = getBaselineSeriesForStream(
series.map(entry => entry.data) as [string | number, number][][],
seriesType,
);

series.unshift(baselineSeries);
}
const selectedValues = (filterState.selectedValues || []).reduce(
(acc: Record<string, number>, selectedValue: string) => {
const index = series.findIndex(({ name }) => name === selectedValue);
Expand Down Expand Up @@ -428,6 +447,9 @@ export default function transformProps(

Object.keys(forecastValues).forEach(key => {
const value = forecastValues[key];
if (value.observation === 0 && stack) {
return;
}
const content = formatForecastTooltipSeries({
...value,
seriesName: key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import {
AnnotationData,
AnnotationOpacity,
AxisType,
CategoricalColorScale,
EventAnnotationLayer,
FilterState,
Expand All @@ -33,7 +34,6 @@ import {
TimeFormatter,
TimeseriesAnnotationLayer,
TimeseriesDataRecord,
AxisType,
} from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
Expand All @@ -53,8 +53,12 @@ import {
import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';

import { extractForecastSeriesContext } from '../utils/forecast';
import { ForecastSeriesEnum, LegendOrientation, StackType } from '../types';
import { EchartsTimeseriesSeriesType } from './types';
import {
EchartsTimeseriesSeriesType,
ForecastSeriesEnum,
LegendOrientation,
StackType,
} from '../types';

import {
evalFormula,
Expand All @@ -64,11 +68,79 @@ import {
} from '../utils/annotation';
import { currentSeries, getChartPadding } from '../utils/series';
import {
AreaChartExtraControlsValue,
OpacityEnum,
StackControlsValue,
TIMESERIES_CONSTANTS,
} from '../constants';

// based on weighted wiggle algorithm
kgabryje marked this conversation as resolved.
Show resolved Hide resolved
// source: https://ieeexplore.ieee.org/document/4658136
export const getBaselineSeriesForStream = (
series: [string | number, number][][],
seriesType: EchartsTimeseriesSeriesType,
) => {
kgabryje marked this conversation as resolved.
Show resolved Hide resolved
const seriesLength = series[0].length;
const baselineSeriesDelta = new Array(seriesLength).fill([0, 0]);
const getVal = (value: number | null) => value ?? 0;
for (let i = 0; i < seriesLength; i += 1) {
let seriesSum = 0;
let weightedSeriesSum = 0;
for (let j = 0; j < series.length; j += 1) {
const delta =
i > 0
? getVal(series[j][i][1]) - getVal(series[j][i - 1][1])
: getVal(series[j][i][1]);
let deltaPrev = 0;
for (let k = 1; k < j - 1; k += 1) {
deltaPrev +=
i > 0
? getVal(series[k][i][1]) - getVal(series[k][i - 1][1])
: getVal(series[k][i][1]);
}
weightedSeriesSum += (0.5 * delta + deltaPrev) * getVal(series[j][i][1]);
seriesSum += getVal(series[j][i][1]);
}
baselineSeriesDelta[i] = [series[0][i][0], -weightedSeriesSum / seriesSum];
}
const baselineSeries = baselineSeriesDelta.reduce((acc, curr, i) => {
if (i === 0) {
acc.push(curr);
} else {
acc.push([curr[0], acc[i - 1][1] + curr[1]]);
}
return acc;
}, []);
return {
data: baselineSeries,
name: 'baseline',
stack: 'obs',
stackStrategy: 'all' as const,
type: 'line' as const,
lineStyle: {
opacity: 0,
},
tooltip: {
show: false,
},
silent: true,
showSymbol: false,
areaStyle: {
opacity: 0,
},
step: [
EchartsTimeseriesSeriesType.Start,
EchartsTimeseriesSeriesType.Middle,
EchartsTimeseriesSeriesType.End,
].includes(seriesType)
? (seriesType as
| EchartsTimeseriesSeriesType.Start
| EchartsTimeseriesSeriesType.Middle
| EchartsTimeseriesSeriesType.End)
: undefined,
smooth: seriesType === EchartsTimeseriesSeriesType.Smooth,
};
};

export function transformSeries(
series: SeriesOption,
colorScale: CategoricalColorScale,
Expand Down Expand Up @@ -190,9 +262,10 @@ export function transformSeries(
showSymbol = true;
}
}
const lineStyle = isConfidenceBand
? { ...opts.lineStyle, opacity: OpacityEnum.Transparent }
: { ...opts.lineStyle, opacity };
const lineStyle =
isConfidenceBand || (stack === StackControlsValue.Stream && area)
? { ...opts.lineStyle, opacity: OpacityEnum.Transparent }
: { ...opts.lineStyle, opacity };
return {
...series,
queryIndex,
Expand All @@ -208,7 +281,10 @@ export function transformSeries(
? seriesType
: undefined,
stack: stackId,
stackStrategy: isConfidenceBand ? 'all' : 'samesign',
stackStrategy:
isConfidenceBand || stack === StackControlsValue.Stream
? 'all'
: 'samesign',
lineStyle,
areaStyle:
area || forecastSeries.type === ForecastSeriesEnum.ForecastUpper
Expand All @@ -234,7 +310,7 @@ export function transformSeries(
const { value, dataIndex, seriesIndex, seriesName } = params;
const numericValue = isHorizontal ? value[0] : value[1];
const isSelectedLegend = currentSeries.legend === seriesName;
const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
const isAreaExpand = stack === StackControlsValue.Expand;
if (!formatter) return numericValue;
if (!stack || isSelectedLegend) return formatter(numericValue);
if (!onlyTotal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
RadioButtonOption,
sharedControlComponents,
} from '@superset-ui/chart-controls';
import { AreaChartExtraControlsOptions } from '../constants';
import { AreaChartStackControlOptions } from '../constants';

const { RadioButtonControl } = sharedControlComponents;

Expand Down Expand Up @@ -53,7 +53,7 @@ export function useExtraControl<

const extraControlsOptions = useMemo(() => {
if (area) {
return AreaChartExtraControlsOptions;
return AreaChartStackControlOptions;
}
return [];
}, [area]);
Expand Down
14 changes: 10 additions & 4 deletions superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,26 @@ export enum OpacityEnum {
NonTransparent = 1,
}

export enum AreaChartExtraControlsValue {
export enum StackControlsValue {
Stack = 'Stack',
Stream = 'Stream',
Expand = 'Expand',
}

export const AreaChartExtraControlsOptions: [
export const StackControlOptions: [
JsonValue,
Exclude<ReactNode, null | undefined | boolean>,
][] = [
[null, t('None')],
[AreaChartExtraControlsValue.Stack, t('Stack')],
[AreaChartExtraControlsValue.Expand, t('Expand')],
[StackControlsValue.Stack, t('Stack')],
[StackControlsValue.Stream, t('Stream')],
];

export const AreaChartStackControlOptions: [
JsonValue,
Exclude<ReactNode, null | undefined | boolean>,
][] = [...StackControlOptions, [StackControlsValue.Expand, t('Expand')]];

export const TIMEGRAIN_TO_TIMESTAMP = {
[TimeGranularity.HOUR]: 3600 * 1000,
[TimeGranularity.DAY]: 3600 * 1000 * 24,
Expand Down
10 changes: 6 additions & 4 deletions superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import {
DEFAULT_LEGEND_FORM_DATA,
DEFAULT_SORT_SERIES_DATA,
StackControlOptions,
} from './constants';
import { DEFAULT_FORM_DATA } from './Timeseries/constants';
import { SortSeriesType } from './types';
Expand Down Expand Up @@ -119,10 +120,11 @@ export const showValueControl: ControlSetItem = {
export const stackControl: ControlSetItem = {
name: 'stack',
config: {
type: 'CheckboxControl',
label: t('Stack series'),
type: 'SelectControl',
label: t('Stacked Style'),
renderTrigger: true,
default: false,
choices: StackControlOptions,
default: null,
description: t('Stack series on top of each other'),
},
};
Expand All @@ -142,7 +144,7 @@ export const onlyTotalControl: ControlSetItem = {
},
};

const percentageThresholdControl: ControlSetItem = {
export const percentageThresholdControl: ControlSetItem = {
name: 'percentage_threshold',
config: {
type: 'TextControl',
Expand Down
4 changes: 2 additions & 2 deletions superset-frontend/plugins/plugin-chart-echarts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '@superset-ui/core';
import { EChartsCoreOption, ECharts } from 'echarts';
import { TooltipMarker } from 'echarts/types/src/util/format';
import { AreaChartExtraControlsValue } from './constants';
import { StackControlsValue } from './constants';

export type EchartsStylesProps = {
height: number;
Expand Down Expand Up @@ -159,7 +159,7 @@ export interface TitleFormData {
yAxisTitlePosition: string;
}

export type StackType = boolean | null | Partial<AreaChartExtraControlsValue>;
export type StackType = boolean | null | Partial<StackControlsValue>;

export interface TreePathInfo {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import { format, LegendComponentOption, SeriesOption } from 'echarts';
import { sumBy, meanBy, minBy, maxBy, orderBy } from 'lodash';
import {
AreaChartExtraControlsValue,
StackControlsValue,
NULL_STRING,
TIMESERIES_CONSTANTS,
} from '../constants';
Expand Down Expand Up @@ -207,7 +207,7 @@ export function extractSeries(
if (isFillNeighborValue) {
value = fillNeighborValue;
} else if (
stack === AreaChartExtraControlsValue.Expand &&
stack === StackControlsValue.Expand &&
totalStackedValues.length > 0
) {
value = ((value || 0) as number) / totalStackedValues[idx];
Expand Down
Loading