You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/06/23 14:57:59 UTC
[superset] branch master updated: fix: Total calculation in stacked Timeseries charts (#24477)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new c5b4ecdca5 fix: Total calculation in stacked Timeseries charts (#24477)
c5b4ecdca5 is described below
commit c5b4ecdca519ab4309a47bfc8feb4a1665c6ce96
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Fri Jun 23 11:57:48 2023 -0300
fix: Total calculation in stacked Timeseries charts (#24477)
---
.../src/chart/models/ChartProps.ts | 18 ++++-
.../superset-ui-core/src/chart/types/Base.ts | 4 ++
.../src/MixedTimeseries/EchartsMixedTimeseries.tsx | 8 +--
.../src/MixedTimeseries/transformProps.ts | 7 +-
.../src/Timeseries/EchartsTimeseries.tsx | 79 +++++-----------------
.../src/Timeseries/transformProps.ts | 10 ++-
.../src/Timeseries/transformers.ts | 15 ++--
.../plugins/plugin-chart-echarts/src/types.ts | 2 +
.../plugin-chart-echarts/src/utils/series.ts | 30 ++++----
.../src/components/Chart/ChartRenderer.jsx | 8 +++
10 files changed, 88 insertions(+), 93 deletions(-)
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts
index e02aeca4f5..815a5df2f4 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts
@@ -30,7 +30,12 @@ import {
FilterState,
JsonObject,
} from '../..';
-import { HandlerFunction, PlainObject, SetDataMaskHook } from '../types/Base';
+import {
+ HandlerFunction,
+ LegendState,
+ PlainObject,
+ SetDataMaskHook,
+} from '../types/Base';
import { QueryData, DataRecordFilters } from '..';
import { SupersetTheme } from '../../style';
@@ -54,6 +59,8 @@ type Hooks = {
onContextMenu?: HandlerFunction;
/** handle errors */
onError?: HandlerFunction;
+ /** handle legend state changes */
+ onLegendStateChanged?: HandlerFunction;
/** use the vis as control to update state */
setControlValue?: HandlerFunction;
/** handle external filters */
@@ -88,6 +95,8 @@ export interface ChartPropsConfig {
ownState?: JsonObject;
/** Filter state that saved in dashboard */
filterState?: FilterState;
+ /** Legend state */
+ legendState?: LegendState;
/** Set of actual behaviors that this instance of chart should use */
behaviors?: Behavior[];
/** Chart display settings related to current view context */
@@ -128,6 +137,8 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
filterState: FilterState;
+ legendState?: LegendState;
+
queriesData: QueryData[];
width: number;
@@ -156,6 +167,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
hooks = {},
ownState = {},
filterState = {},
+ legendState,
initialValues = {},
queriesData = [],
behaviors = [],
@@ -181,6 +193,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
this.queriesData = queriesData;
this.ownState = ownState;
this.filterState = filterState;
+ this.legendState = legendState;
this.behaviors = behaviors;
this.displaySettings = displaySettings;
this.appSection = appSection;
@@ -205,6 +218,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
input => input.width,
input => input.ownState,
input => input.filterState,
+ input => input.legendState,
input => input.behaviors,
input => input.displaySettings,
input => input.appSection,
@@ -224,6 +238,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
width,
ownState,
filterState,
+ legendState,
behaviors,
displaySettings,
appSection,
@@ -243,6 +258,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
queriesData,
ownState,
filterState,
+ legendState,
width,
behaviors,
displaySettings,
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
index 418d6a36fc..b3884a8488 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
@@ -100,4 +100,8 @@ export enum AxisType {
log = 'log',
}
+export interface LegendState {
+ [key: string]: boolean;
+}
+
export default {};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
index 8686042611..8c55ff7ae2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
@@ -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();
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index a14904b05c..77e8550d09 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -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 />');
},
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
index 7f75d27105..bd23c3f257 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
@@ -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 = '';
+ legendselectchanged: payload => {
+ onLegendStateChanged?.(payload.selected);
},
- mouseover: params => {
- currentSeries.name = params.seriesName;
+ legendselectall: payload => {
+ onLegendStateChanged?.(payload.selected);
},
- 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();
- }
+ 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}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 6e1d0186ef..c89bff2e8c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -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,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index 37e3eb9fce..fb4739dc74 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -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 >=
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index 4b96f73bb9..d7280ea9d6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -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;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 5235e168d9..0f6efd72f2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -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;
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index 55f6b66df7..006dd54fcb 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -90,6 +90,7 @@ class ChartRenderer extends React.Component {
(isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) ||
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)),
inContextMenu: false,
+ legendState: undefined,
};
this.hasQueryResponseChange = false;
@@ -102,6 +103,7 @@ class ChartRenderer extends React.Component {
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
+ this.handleLegendStateChanged = this.handleLegendStateChanged.bind(this);
this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
this.hooks = {
@@ -113,6 +115,7 @@ class ChartRenderer extends React.Component {
setControlValue: this.handleSetControlValue,
onFilterMenuOpen: this.props.onFilterMenuOpen,
onFilterMenuClose: this.props.onFilterMenuClose,
+ onLegendStateChanged: this.handleLegendStateChanged,
setDataMask: dataMask => {
this.props.actions?.updateDataMask(this.props.chartId, dataMask);
},
@@ -226,6 +229,10 @@ class ChartRenderer extends React.Component {
this.setState({ inContextMenu: false });
}
+ handleLegendStateChanged(legendState) {
+ this.setState({ legendState });
+ }
+
// When viz plugins don't handle `contextmenu` event, fallback handler
// calls `handleOnContextMenu` with no `filters` param.
onContextMenuFallback(event) {
@@ -354,6 +361,7 @@ class ChartRenderer extends React.Component {
noResults={noResultsComponent}
postTransformProps={postTransformProps}
emitCrossFilters={emitCrossFilters}
+ legendState={this.state.legendState}
{...drillToDetailProps}
/>
</div>