mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
fix: Total calculation in stacked Timeseries charts (#24477)
This commit is contained in:
committed by
GitHub
parent
51a34d7d58
commit
c5b4ecdca5
@@ -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();
|
||||
|
||||
@@ -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 />');
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 >=
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user