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 e02aeca4f54de..859ed47ec3dc8 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 { filterState: FilterState; + legendState: LegendState; + queriesData: QueryData[]; width: number; @@ -156,6 +167,7 @@ export default class ChartProps { hooks = {}, ownState = {}, filterState = {}, + legendState, initialValues = {}, queriesData = [], behaviors = [], @@ -181,6 +193,7 @@ export default class ChartProps { 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 418d6a36fce3d..b3884a8488013 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 8686042611c5d..8c55ff7ae227a 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 a14904b05c7b1..77e8550d09937 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(`${content}`); - } else { - rows.push(`${content}`); - } + rows.push(`${content}`); }); return rows.join('
'); }, 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 7f75d27105a0d..bd23c3f2573f7 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(null); // eslint-disable-next-line no-param-reassign refs.echartRef = echartRef; - const lastTimeRef = useRef(Date.now()); - const lastSelectedLegend = useRef(''); const clickTimer = useRef>(); const extraControlRef = useRef(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({ 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(`${content}`); } else { rows.push(`${content}`); @@ -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 37e3eb9fcea04..fb4739dc74658 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 4b96f73bb9d6e..d7280ea9d67da 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 { 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 5235e168d9f93..65662886a6433 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, + selected?: LegendState, ): LegendComponentOption | LegendComponentOption[] { const legend: LegendComponentOption | LegendComponentOption[] = { orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes( @@ -413,6 +424,7 @@ export function getLegendProps( : 'vertical', show, type, + selected, 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 55f6b66df7d6d..006dd54fcb39a 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} />