Skip to content

Commit

Permalink
feat(plugin-chart-echarts): [feature-parity] support double clicking …
Browse files Browse the repository at this point in the history
…legend and series to view single selected series (#1324)

* feat(plugin-chart-echarts): support double clicking legend

* fix: get echart instance instead of method

* feat: support click series to view single series

* fix: lint

* fix: clear single select

* fix: remove unused comment

* fix: UT
  • Loading branch information
stephenLYZ authored and zhaoyongjie committed Nov 26, 2021
1 parent de86eb9 commit ff8979b
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { EventHandlers } from '../types';
import React, { useCallback, useRef } from 'react';
import { ViewRootGroup } from 'echarts/types/src/util/types';
import GlobalModel from 'echarts/types/src/model/Global';
import ComponentModel from 'echarts/types/src/model/Component';
import { EchartsHandler, EventHandlers } from '../types';
import Echart from '../components/Echart';
import { TimeseriesChartTransformedProps } from './types';
import { currentSeries } from '../utils/series';

const TIMER_DURATION = 300;
// @ts-ignore
export default function EchartsTimeseries({
formData,
Expand All @@ -32,10 +36,55 @@ export default function EchartsTimeseries({
labelMap,
selectedValues,
setDataMask,
legendData = [],
}: TimeseriesChartTransformedProps) {
const { emitFilter, stack } = formData;
const echartRef = useRef<EchartsHandler | null>(null);
const lastTimeRef = useRef(Date.now());
const lastSelectedLegend = useRef('');
const clickTimer = useRef<ReturnType<typeof setTimeout>>();

const handleDoubleClickChange = useCallback((name?: string) => {
const echartInstance = echartRef.current?.getEchartInstance();
if (!name) {
echartInstance?.dispatchAction({
type: 'legendAllSelect',
});
} else {
legendData.forEach(datum => {
if (datum === name) {
echartInstance?.dispatchAction({
type: 'legendSelect',
name: datum,
});
} else {
echartInstance?.dispatchAction({
type: 'legendUnSelect',
name: datum,
});
}
});
}
}, []);

const getModelInfo = (target: ViewRootGroup, globalModel: GlobalModel) => {
let el = target;
let model: ComponentModel | null = null;
while (el) {
// eslint-disable-next-line no-underscore-dangle
const modelInfo = el.__ecComponentInfo;
if (modelInfo != null) {
model = globalModel.getComponent(modelInfo.mainType, modelInfo.index);
break;
}
el = el.parent;
}
return model;
};

const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
if (!emitFilter) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
Expand Down Expand Up @@ -71,28 +120,83 @@ export default function EchartsTimeseries({

const eventHandlers: EventHandlers = {
click: props => {
const { seriesName: name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
if (clickTimer.current) {
clearTimeout(clickTimer.current);
}
// Ensure that double-click events do not trigger single click event. So we put it in the timer.
clickTimer.current = setTimeout(() => {
const { seriesName: name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
}, TIMER_DURATION);
},
mousemove: params => {
currentSeries.name = params.seriesName;
},
mouseout: () => {
currentSeries.name = '';
},
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();
}
},
};

const zrEventHandlers: EventHandlers = {
dblclick: params => {
// clear single click timer
if (clickTimer.current) {
clearTimeout(clickTimer.current);
}
const pointInPixel = [params.offsetX, params.offsetY];
const echartInstance = echartRef.current?.getEchartInstance();
if (echartInstance?.containPixel('grid', pointInPixel)) {
// do not trigger if click unstacked chart's blank area
if (!stack && params.target?.type === 'ec-polygon') return;
// @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);
}
}
}
},
};

return (
<Echart
ref={echartRef}
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@ export default function transformProps(
yAxisTitleMargin,
xAxisTitleMargin,
);

const legendData = rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type === ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers, annotationData));

const echartOptions: EChartsCoreOption = {
useUTC: true,
grid: {
Expand Down Expand Up @@ -282,14 +291,7 @@ export default function transformProps(
},
legend: {
...getLegendProps(legendType, legendOrientation, showLegend, zoomable),
// @ts-ignore
data: rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type === ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers, annotationData)),
data: legendData as string[],
},
series: dedupSeries(series),
toolbox: {
Expand Down Expand Up @@ -328,5 +330,6 @@ export default function transformProps(
selectedValues,
setDataMask,
width,
legendData,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,36 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
import { styled } from '@superset-ui/core';
import { ECharts, init } from 'echarts';
import { EchartsProps, EchartsStylesProps } from '../types';
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';

const Styles = styled.div<EchartsStylesProps>`
height: ${({ height }) => height};
width: ${({ width }) => width};
`;

export default function Echart({
width,
height,
echartOptions,
eventHandlers,
selectedValues = {},
}: EchartsProps) {
function Echart(
{
width,
height,
echartOptions,
eventHandlers,
zrEventHandlers,
selectedValues = {},
}: EchartsProps,
ref: React.Ref<EchartsHandler>,
) {
const divRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<ECharts>();
const currentSelection = Object.keys(selectedValues) || [];
const currentSelection = useMemo(() => Object.keys(selectedValues) || [], [selectedValues]);
const previousSelection = useRef<string[]>([]);

useImperativeHandle(ref, () => ({
getEchartInstance: () => chartRef.current,
}));

useEffect(() => {
if (!divRef.current) return;
if (!chartRef.current) {
Expand All @@ -49,8 +57,17 @@ export default function Echart({
chartRef.current?.on(name, handler);
});

Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
chartRef.current?.getZr().off(name);
chartRef.current?.getZr().on(name, handler);
});

chartRef.current.setOption(echartOptions, true);
}, [echartOptions, eventHandlers, zrEventHandlers]);

// highlighting
useEffect(() => {
if (!chartRef.current) return;
chartRef.current.dispatchAction({
type: 'downplay',
dataIndex: previousSelection.current.filter(value => !currentSelection.includes(value)),
Expand All @@ -62,7 +79,7 @@ export default function Echart({
});
}
previousSelection.current = currentSelection;
}, [echartOptions, eventHandlers, selectedValues]);
}, [currentSelection]);

useEffect(() => {
if (chartRef.current) {
Expand All @@ -72,3 +89,5 @@ export default function Echart({

return <Styles ref={divRef} height={height} width={width} />;
}

export default forwardRef(Echart);
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
* under the License.
*/
import { DataRecordValue, SetDataMaskHook } from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import { EChartsCoreOption, ECharts } from 'echarts';
import { TooltipMarker } from 'echarts/types/src/util/format';
import { OptionName } from 'echarts/types/src/util/types';

export type EchartsStylesProps = {
height: number;
Expand All @@ -30,10 +31,15 @@ export interface EchartsProps {
width: number;
echartOptions: EChartsCoreOption;
eventHandlers?: EventHandlers;
zrEventHandlers?: EventHandlers;
selectedValues?: Record<number, string>;
forceClear?: boolean;
}

export interface EchartsHandler {
getEchartInstance: () => ECharts | undefined;
}

export enum ForecastSeriesEnum {
Observation = '',
ForecastTrend = '__yhat',
Expand Down Expand Up @@ -108,6 +114,7 @@ export interface EChartTransformedProps<F> {
labelMap: Record<string, DataRecordValue[]>;
groupby: string[];
selectedValues: Record<number, string>;
legendData?: OptionName[];
}

export interface EchartsTitleFormData {
Expand Down

0 comments on commit ff8979b

Please sign in to comment.