fix: Total calculation in stacked Timeseries charts (#24477)

This commit is contained in:
Michael S. Molina
2023-06-23 11:57:48 -03:00
committed by GitHub
parent 51a34d7d58
commit c5b4ecdca5
10 changed files with 89 additions and 94 deletions

View File

@@ -29,7 +29,7 @@ import {
import { EchartsMixedTimeseriesChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { currentSeries, formatSeriesName } from '../utils/series';
import { formatSeriesName } from '../utils/series';
export default function EchartsMixedTimeseries({
height,
@@ -123,12 +123,6 @@ export default function EchartsMixedTimeseries({
const { seriesName, seriesIndex } = props;
handleChange(seriesName, seriesIndex);
},
mouseout: () => {
currentSeries.name = '';
},
mouseover: params => {
currentSeries.name = params.seriesName;
},
contextmenu: async eventParams => {
if (onContextMenu) {
eventParams.event.stop();

View File

@@ -51,7 +51,6 @@ import {
import { parseYAxisBound } from '../utils/controls';
import {
getOverMaxHiddenFormatter,
currentSeries,
dedupSeries,
extractSeries,
getAxisType,
@@ -481,11 +480,7 @@ export default function transformProps(
seriesName: key,
formatter: primarySeries.has(key) ? formatter : formatterSecondary,
});
if (currentSeries.name === key) {
rows.push(`<span style="font-weight: 700">${content}</span>`);
} else {
rows.push(`<span style="opacity: 0.7">${content}</span>`);
}
rows.push(`<span style="opacity: 0.7">${content}</span>`);
});
return rows.join('<br />');
},

View File

@@ -24,6 +24,7 @@ import {
getTimeFormatter,
getColumnLabel,
getNumberFormatter,
LegendState,
} from '@superset-ui/core';
import { ViewRootGroup } from 'echarts/types/src/util/types';
import GlobalModel from 'echarts/types/src/model/Global';
@@ -31,12 +32,11 @@ import ComponentModel from 'echarts/types/src/model/Component';
import { EchartsHandler, EventHandlers } from '../types';
import Echart from '../components/Echart';
import { TimeseriesChartTransformedProps } from './types';
import { currentSeries, formatSeriesName } from '../utils/series';
import { formatSeriesName } from '../utils/series';
import { ExtraControls } from '../components/ExtraControls';
const TIMER_DURATION = 300;
// @ts-ignore
export default function EchartsTimeseries({
formData,
height,
@@ -49,6 +49,7 @@ export default function EchartsTimeseries({
setControlValue,
legendData = [],
onContextMenu,
onLegendStateChanged,
xValueFormatter,
xAxis,
refs,
@@ -59,8 +60,6 @@ export default function EchartsTimeseries({
const echartRef = useRef<EchartsHandler | null>(null);
// eslint-disable-next-line no-param-reassign
refs.echartRef = echartRef;
const lastTimeRef = useRef(Date.now());
const lastSelectedLegend = useRef('');
const clickTimer = useRef<ReturnType<typeof setTimeout>>();
const extraControlRef = useRef<HTMLDivElement>(null);
const [extraControlHeight, setExtraControlHeight] = useState(0);
@@ -69,34 +68,6 @@ export default function EchartsTimeseries({
setExtraControlHeight(updatedHeight);
}, [formData.showExtraControls]);
const handleDoubleClickChange = useCallback(
(name?: string) => {
const echartInstance = echartRef.current?.getEchartInstance();
if (!name) {
currentSeries.legend = '';
echartInstance?.dispatchAction({
type: 'legendAllSelect',
});
} else {
legendData.forEach(datum => {
if (datum === name) {
currentSeries.legend = datum;
echartInstance?.dispatchAction({
type: 'legendSelect',
name: datum,
});
} else {
echartInstance?.dispatchAction({
type: 'legendUnSelect',
name: datum,
});
}
});
}
},
[legendData],
);
const getModelInfo = (target: ViewRootGroup, globalModel: GlobalModel) => {
let el = target;
let model: ComponentModel | null = null;
@@ -175,30 +146,14 @@ export default function EchartsTimeseries({
handleChange(name);
}, TIMER_DURATION);
},
mouseout: () => {
currentSeries.name = '';
},
mouseover: params => {
currentSeries.name = params.seriesName;
},
legendselectchanged: payload => {
const currentTime = Date.now();
// TIMER_DURATION is the interval between two legendselectchanged event
if (
currentTime - lastTimeRef.current < TIMER_DURATION &&
lastSelectedLegend.current === payload.name
) {
// execute dbclick
handleDoubleClickChange(payload.name);
} else {
lastTimeRef.current = currentTime;
// remember last selected legend
lastSelectedLegend.current = payload.name;
}
// if all legend is unselected, we keep all selected
if (Object.values(payload.selected).every(i => !i)) {
handleDoubleClickChange();
}
onLegendStateChanged?.(payload.selected);
},
legendselectall: payload => {
onLegendStateChanged?.(payload.selected);
},
legendinverseselect: payload => {
onLegendStateChanged?.(payload.selected);
},
contextmenu: async eventParams => {
if (onContextMenu) {
@@ -272,15 +227,16 @@ export default function EchartsTimeseries({
// @ts-ignore
const globalModel = echartInstance.getModel();
const model = getModelInfo(params.target, globalModel);
const seriesCount = globalModel.getSeriesCount();
const currentSeriesIndices = globalModel.getCurrentSeriesIndices();
if (model) {
const { name } = model;
if (seriesCount !== currentSeriesIndices.length) {
handleDoubleClickChange();
} else {
handleDoubleClickChange(name);
}
const legendState: LegendState = legendData.reduce(
(previous, datum) => ({
...previous,
[datum]: datum === name,
}),
{},
);
onLegendStateChanged?.(legendState);
}
}
},
@@ -292,6 +248,7 @@ export default function EchartsTimeseries({
<ExtraControls formData={formData} setControlValue={setControlValue} />
</div>
<Echart
ref={echartRef}
refs={refs}
height={height - extraControlHeight}
width={width}

View File

@@ -54,7 +54,6 @@ import { ForecastSeriesEnum, ForecastValue, Refs } from '../types';
import { parseYAxisBound } from '../utils/controls';
import {
calculateLowerLogTick,
currentSeries,
dedupSeries,
extractDataTotalValues,
extractSeries,
@@ -101,6 +100,7 @@ export default function transformProps(
width,
height,
filterState,
legendState,
formData,
hooks,
queriesData,
@@ -192,6 +192,7 @@ export default function transformProps(
stack,
percentageThreshold,
xAxisCol: xAxisLabel,
legendState,
},
);
const extraMetricLabels = extractExtraMetrics(chartProps.rawFormData).map(
@@ -221,6 +222,7 @@ export default function transformProps(
stack,
onlyTotal,
isHorizontal,
legendState,
});
const seriesContexts = extractForecastSeriesContexts(
Object.values(rawSeries).map(series => series.name as string),
@@ -258,6 +260,7 @@ export default function transformProps(
markerSize,
areaOpacity: opacity,
seriesType,
legendState,
stack,
formatter,
showValue,
@@ -379,6 +382,7 @@ export default function transformProps(
setDataMask = () => {},
setControlValue = () => {},
onContextMenu,
onLegendStateChanged,
} = hooks;
const addYAxisLabelOffset = !!yAxisTitle;
@@ -486,7 +490,7 @@ export default function transformProps(
seriesName: key,
formatter,
});
if (currentSeries.name === key) {
if (!legendState || legendState[key]) {
rows.push(`<span style="font-weight: 700">${content}</span>`);
} else {
rows.push(`<span style="opacity: 0.7">${content}</span>`);
@@ -506,6 +510,7 @@ export default function transformProps(
showLegend,
theme,
zoomable,
legendState,
),
data: legendData as string[],
},
@@ -549,6 +554,7 @@ export default function transformProps(
width,
legendData,
onContextMenu,
onLegendStateChanged,
xValueFormatter: tooltipFormatter,
xAxis: {
label: xAxisLabel,

View File

@@ -27,6 +27,7 @@ import {
getTimeFormatter,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
LegendState,
NumberFormatter,
smartDateDetailedFormatter,
smartDateFormatter,
@@ -65,7 +66,7 @@ import {
formatAnnotationLabel,
parseAnnotationOpacity,
} from '../utils/annotation';
import { currentSeries, getChartPadding } from '../utils/series';
import { getChartPadding } from '../utils/series';
import {
OpacityEnum,
StackControlsValue,
@@ -156,6 +157,7 @@ export function transformSeries(
yAxisIndex?: number;
showValue?: boolean;
onlyTotal?: boolean;
legendState?: LegendState;
formatter?: NumberFormatter;
totalStackedValues?: number[];
showValueIndexes?: number[];
@@ -182,6 +184,7 @@ export function transformSeries(
showValue,
onlyTotal,
formatter,
legendState,
totalStackedValues = [],
showValueIndexes = [],
thresholdValues = [],
@@ -308,10 +311,14 @@ export function transformSeries(
formatter: (params: any) => {
const { value, dataIndex, seriesIndex, seriesName } = params;
const numericValue = isHorizontal ? value[0] : value[1];
const isSelectedLegend = currentSeries.legend === seriesName;
const isSelectedLegend = !legendState || legendState[seriesName];
const isAreaExpand = stack === StackControlsValue.Expand;
if (!formatter) return numericValue;
if (!stack || isSelectedLegend) return formatter(numericValue);
if (!formatter) {
return numericValue;
}
if (!stack && isSelectedLegend) {
return formatter(numericValue);
}
if (!onlyTotal) {
if (
numericValue >=

View File

@@ -23,6 +23,7 @@ import {
ContextMenuFilters,
FilterState,
HandlerFunction,
LegendState,
PlainObject,
QueryFormColumn,
SetDataMaskHook,
@@ -127,6 +128,7 @@ export interface BaseTransformedProps<F> {
filters?: ContextMenuFilters,
) => void;
setDataMask?: SetDataMaskHook;
onLegendStateChanged?: (state: LegendState) => void;
filterState?: FilterState;
refs: Refs;
width: number;

View File

@@ -30,6 +30,7 @@ import {
TimeFormatter,
SupersetTheme,
normalizeTimestamp,
LegendState,
} from '@superset-ui/core';
import { SortSeriesType } from '@superset-ui/chart-controls';
import { format, LegendComponentOption, SeriesOption } from 'echarts';
@@ -52,6 +53,7 @@ export function extractDataTotalValues(
stack: StackType;
percentageThreshold: number;
xAxisCol: string;
legendState?: LegendState;
},
): {
totalStackedValues: number[];
@@ -59,13 +61,16 @@ export function extractDataTotalValues(
} {
const totalStackedValues: number[] = [];
const thresholdValues: number[] = [];
const { stack, percentageThreshold, xAxisCol } = opts;
const { stack, percentageThreshold, xAxisCol, legendState } = opts;
if (stack) {
data.forEach(datum => {
const values = Object.keys(datum).reduce((prev, curr) => {
if (curr === xAxisCol) {
return prev;
}
if (legendState && !legendState[curr]) {
return prev;
}
const value = datum[curr] || 0;
return prev + (value as number);
}, 0);
@@ -85,23 +90,28 @@ export function extractShowValueIndexes(
stack: StackType;
onlyTotal?: boolean;
isHorizontal?: boolean;
legendState?: LegendState;
},
): number[] {
const showValueIndexes: number[] = [];
if (opts.stack) {
const { legendState, stack, isHorizontal, onlyTotal } = opts;
if (stack) {
series.forEach((entry, seriesIndex) => {
const { data = [] } = entry;
(data as [any, number][]).forEach((datum, dataIndex) => {
if (!opts.onlyTotal && datum[opts.isHorizontal ? 0 : 1] !== null) {
if (entry.id && legendState && !legendState[entry.id]) {
return;
}
if (!onlyTotal && datum[isHorizontal ? 0 : 1] !== null) {
showValueIndexes[dataIndex] = seriesIndex;
}
if (opts.onlyTotal) {
if (datum[opts.isHorizontal ? 0 : 1] > 0) {
if (onlyTotal) {
if (datum[isHorizontal ? 0 : 1] > 0) {
showValueIndexes[dataIndex] = seriesIndex;
}
if (
!showValueIndexes[dataIndex] &&
datum[opts.isHorizontal ? 0 : 1] !== null
datum[isHorizontal ? 0 : 1] !== null
) {
showValueIndexes[dataIndex] = seriesIndex;
}
@@ -404,6 +414,7 @@ export function getLegendProps(
show: boolean,
theme: SupersetTheme,
zoomable = false,
legendState?: LegendState,
): LegendComponentOption | LegendComponentOption[] {
const legend: LegendComponentOption | LegendComponentOption[] = {
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
@@ -413,6 +424,7 @@ export function getLegendProps(
: 'vertical',
show,
type,
selected: legendState,
selector: ['all', 'inverse'],
selectorLabel: {
fontFamily: theme.typography.families.sansSerif,
@@ -495,12 +507,6 @@ export function sanitizeHtml(text: string): string {
return format.encodeHTML(text);
}
// TODO: Better use other method to maintain this state
export const currentSeries = {
name: '',
legend: '',
};
export function getAxisType(dataType?: GenericDataType): AxisType {
if (dataType === GenericDataType.TEMPORAL) {
return AxisType.time;