@neo4j-ndl/react-charts
Version:
React implementation of charts from Neo4j Design System
145 lines • 6.28 kB
JavaScript
/**
*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getInstanceByDom } from 'echarts';
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
import { highlightOrDownplaySeries, resetAllSeriesHighlight, } from '../utils/legend-utils';
import { isThresholdLine } from '../utils/threshold';
import { useChartRefsContext } from './use-chart-refs';
const LEGEND_HOVER_HIGHLIGHT_DELAY_MS = 80;
const LEGEND_SELECTION_EVENT_TYPES = [
'legendselectchanged',
'legendselectall',
'legendselected',
'legendunselected',
];
const getAllSeriesSelected = (series) => Object.fromEntries(series.map((s) => { var _a; return [(_a = s.name) !== null && _a !== void 0 ? _a : '', true]; }));
const hasSelectionForCurrentSeries = (selection, series) => {
const selectedNames = Object.keys(selection);
return (selectedNames.length === series.length &&
series.every((s) => { var _a; return Object.prototype.hasOwnProperty.call(selection, (_a = s.name) !== null && _a !== void 0 ? _a : ''); }));
};
const filterThresholdLineSelection = (selected) => Object.fromEntries(Object.entries(selected).filter(([key]) => { var _a; return !((_a = isThresholdLine(key)) !== null && _a !== void 0 ? _a : false); }));
const clearTimeoutRef = (timeoutRef) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
const createLegendSelectionEventHandler = (chart, eventType, setSelectedSeries) => (params) => {
var _a;
if (typeof params !== 'object' ||
params === null ||
!('selected' in params) ||
params.selected === null) {
return;
}
if (eventType === 'legendselectchanged') {
resetAllSeriesHighlight(chart);
}
setSelectedSeries(filterThresholdLineSelection((_a = params.selected) !== null && _a !== void 0 ? _a : {}));
};
/**
* Owns React legend selection and keeps it synchronized with ECharts.
*
* Current series changes preserve existing selection by name, new series default
* to visible, removed series are dropped, and ECharts legend events update the
* React state used to render legend items.
*/
export function useLegendSelection({ series }) {
const { chartEchartRef, legendSelectedRef } = useChartRefsContext();
const initialSelected = useMemo(() => getAllSeriesSelected(series), [series]);
const [selectedSeries, setSelectedSeries] = useState(initialSelected);
const renderedSelectedSeries = hasSelectionForCurrentSeries(selectedSeries, series)
? selectedSeries
: initialSelected;
useEffect(() => {
var _a, _b, _c;
const prev = (_a = legendSelectedRef.current) !== null && _a !== void 0 ? _a : {};
const next = {};
for (const s of series) {
const name = (_b = s.name) !== null && _b !== void 0 ? _b : '';
next[name] = (_c = prev[name]) !== null && _c !== void 0 ? _c : true;
}
setSelectedSeries(next);
}, [legendSelectedRef, series]);
useEffect(() => {
legendSelectedRef.current = selectedSeries;
}, [legendSelectedRef, selectedSeries]);
useEffect(() => {
if (chartEchartRef.current === null) {
return;
}
const chart = getInstanceByDom(chartEchartRef.current);
if (!chart) {
return;
}
const eventHandlers = LEGEND_SELECTION_EVENT_TYPES.map((eventType) => {
const handler = createLegendSelectionEventHandler(chart, eventType, setSelectedSeries);
chart.on(eventType, handler);
return { eventType, handler };
});
return () => {
eventHandlers.forEach(({ eventType, handler }) => {
chart.off(eventType, handler);
});
};
}, [chartEchartRef]);
return { renderedSelectedSeries };
}
/**
* Creates a delayed ECharts highlighter for legend item hover state.
*
* Legend hover moves often happen as a quick leave/enter pair between adjacent
* items. Keeping separate highlight and downplay timers lets the next hover
* cancel the previous opposite action, preventing the chart from flickering
* between emphasized and reset states. Deselected legend items are ignored so
* hidden series do not get emphasized through hover.
*/
export function useLegendHoverHighlight({ selectedSeries, series, }) {
const { chartEchartRef } = useChartRefsContext();
const highlightTimeOut = useRef(null);
const downplayTimeOut = useRef(null);
useEffect(() => {
return () => {
clearTimeoutRef(highlightTimeOut);
clearTimeoutRef(downplayTimeOut);
};
}, []);
const toggleHighlight = useCallback((seriesToUpdate, shouldHighlight) => {
const isDeselected = seriesToUpdate.name === undefined
? false
: !selectedSeries[seriesToUpdate.name];
if (isDeselected) {
return;
}
const timeoutToClear = shouldHighlight
? downplayTimeOut
: highlightTimeOut;
const timeoutToSet = shouldHighlight ? highlightTimeOut : downplayTimeOut;
if (timeoutToClear.current) {
clearTimeout(timeoutToClear.current);
}
timeoutToSet.current = setTimeout(() => {
highlightOrDownplaySeries(chartEchartRef, series, selectedSeries, [seriesToUpdate], shouldHighlight ? 'highlight' : 'downplay');
}, LEGEND_HOVER_HIGHLIGHT_DELAY_MS);
}, [chartEchartRef, selectedSeries, series]);
return { toggleHighlight };
}
//# sourceMappingURL=use-legend-interactions.js.map