UNPKG

@neo4j-ndl/react-charts

Version:

React implementation of charts from Neo4j Design System

240 lines 15.9 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * * 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 { tokens } from '@neo4j-ndl/base'; import { ConditionalWrap, Tooltip, Typography, useResizeObserver, } from '@neo4j-ndl/react'; import { CheckIconOutline } from '@neo4j-ndl/react/icons'; import classNames from 'classnames'; import { getInstanceByDom } from 'echarts'; import { useEffect, useRef, useState } from 'react'; import { getComputedElementWidth, highlightOrDownplaySeries, isThresholdLine, resetAllSeriesHighlight, useLegendVisibility, } from './legend-utils'; const LegendItem = function LegendItemComponent(_a) { var _b; var { children, as, className, name, selected, deSelected, onLegendItemClick, color, hasButtons = true, ref, onLegendItemMouseEnter, onLegendItemMouseLeave } = _a, restProps = __rest(_a, ["children", "as", "className", "name", "selected", "deSelected", "onLegendItemClick", "color", "hasButtons", "ref", "onLegendItemMouseEnter", "onLegendItemMouseLeave"]); const Component = as !== null && as !== void 0 ? as : (hasButtons ? 'button' : 'div'); const classes = classNames('ndl-chart-legend-item', { 'ndl-chart-legend-item-deselected': deSelected, 'ndl-chart-legend-item-selected': selected, }, className); // We don't want to display threshold lines in the legend if ((_b = isThresholdLine(name)) !== null && _b !== void 0 ? _b : false) { return null; } return (_jsxs(Component, Object.assign({ className: classes, ref: ref, "data-labelname": name, title: name, onClick: hasButtons ? onLegendItemClick : undefined, onMouseEnter: onLegendItemMouseEnter, onMouseLeave: onLegendItemMouseLeave }, restProps, { children: [_jsx("span", { className: "ndl-chart-legend-item-square", style: { '--ndl-chart-legend-item-color': color, backgroundColor: deSelected === true ? 'transparent' : color, }, children: selected === true && (_jsx(CheckIconOutline, { className: "ndl-chart-legend-item-square-checkmark" })) }), _jsx(Typography, { variant: "body-medium", className: "ndl-chart-legend-item-text", children: children })] }))); }; export const Legend = function LegendComponent(_a) { var { className, wrappingType = 'wrapping', series, chartRef, ref, htmlAttributes } = _a, restProps = __rest(_a, ["className", "wrappingType", "series", "chartRef", "ref", "htmlAttributes"]); const [selectedSeries, setSelectedSeries] = useState(Object.fromEntries(series.map((s) => { var _a; return [(_a = s.name) !== null && _a !== void 0 ? _a : '', true]; }))); const highlightTimeOut = useRef(null); const downplayTimeOut = useRef(null); const hoverTimeOut = 80; const highlightSeries = (seriesToUpdate) => { // Clear the downplay timeout when a new item is hovered if (downplayTimeOut.current) { clearTimeout(downplayTimeOut.current); } // Delay the highlight to avoid flickering when quickly hovering the legend items highlightTimeOut.current = setTimeout(() => { highlightOrDownplaySeries(chartRef, series, selectedSeries, seriesToUpdate, 'highlight'); }, hoverTimeOut); }; const downplaySeries = (seriesToUpdate) => { // Clear the highlight timeout when the mouse is leaving the legend item if (highlightTimeOut.current) { clearTimeout(highlightTimeOut.current); } // Delay the downplay to avoid flickering when moving the highlight between legend items (no downplay needed in between) downplayTimeOut.current = setTimeout(() => { highlightOrDownplaySeries(chartRef, series, selectedSeries, seriesToUpdate, 'downplay'); }, hoverTimeOut); }; useEffect(() => { setSelectedSeries(Object.fromEntries(series.map((s) => { var _a; return [(_a = s.name) !== null && _a !== void 0 ? _a : '', true]; }))); if (chartRef.current === null) { return; } const chart = getInstanceByDom(chartRef.current); const eventTypes = [ 'legendselectchanged', 'legendselectall', 'legendselected', 'legendunselected', ]; eventTypes.forEach((eventType) => { chart === null || chart === void 0 ? void 0 : chart.on(eventType, (params) => { var _a; if (typeof params === 'object' && params !== null && 'selected' in params && params.selected !== null) { const selected = (_a = params.selected) !== null && _a !== void 0 ? _a : {}; const filteredSelected = Object.fromEntries(Object.entries(selected).filter(([key]) => { var _a; return !((_a = isThresholdLine(key)) !== null && _a !== void 0 ? _a : false); })); // Reset the series highlight to avoid series to stay blurred on selection change if (eventType === 'legendselectchanged') { resetAllSeriesHighlight(chart); } setSelectedSeries(filteredSelected); } }); }); return () => { eventTypes.forEach((eventType) => { chart === null || chart === void 0 ? void 0 : chart.off(eventType); }); }; }, [chartRef, series]); const classes = classNames(`ndl-chart-legend`, { 'ndl-chart-legend-truncation': wrappingType === 'truncation', 'ndl-chart-legend-wrapping': wrappingType === 'wrapping', }, className); const { toggleLegendVisibility, setAllVisible } = useLegendVisibility(chartRef, selectedSeries); return (_jsx("div", Object.assign({ ref: ref, className: "ndl-chart-legend-container" }, restProps, htmlAttributes, { children: wrappingType === 'truncation' || wrappingType === 'wrapping' ? (_jsx("div", { className: classes, children: series.map((currentSeries, index) => { var _a, _b; const hasMoreThanOneItem = series.length > 1; const selectedSeriesLength = Object.values(selectedSeries).filter(Boolean).length; const isAllSeriesVisible = selectedSeriesLength === series.length; const color = currentSeries.color; const isDeselected = currentSeries.name === undefined ? false : !selectedSeries[currentSeries.name]; return (_jsx(ConditionalWrap, { shouldWrap: wrappingType === 'truncation', wrap: (children) => (_jsx(Tooltip, { type: "simple", children: _jsx(Tooltip.Trigger, { hasButtonWrapper: true, children: children }) }, index)), children: _jsx(LegendItem, { name: currentSeries.name, color: color, hasButtons: hasMoreThanOneItem && currentSeries.name !== undefined, onLegendItemMouseEnter: () => { !isDeselected && highlightSeries([currentSeries]); }, onLegendItemMouseLeave: () => { !isDeselected && downplaySeries([currentSeries]); }, onLegendItemClick: () => { var _a; const isAllSeriesSelected = selectedSeriesLength === series.length; const isOnlyVisible = selectedSeries[(_a = currentSeries.name) !== null && _a !== void 0 ? _a : ''] && selectedSeriesLength === 1; toggleLegendVisibility(currentSeries.name, isAllSeriesSelected, isOnlyVisible); }, selected: !isAllSeriesVisible && selectedSeries[(_a = currentSeries.name) !== null && _a !== void 0 ? _a : ''], deSelected: isDeselected, children: (_b = currentSeries.name) !== null && _b !== void 0 ? _b : `Series ${index}` }) }, index)); }) })) : (_jsx(LegendOverflowType, { className: classes, selectedSeries: selectedSeries, wrappingType: wrappingType, chartRef: chartRef, series: series, onSetAllVisible: setAllVisible, onLegendItemMouseEnter: (seriesToUpdate) => highlightSeries(seriesToUpdate), onLegendItemMouseLeave: (seriesToUpdate) => downplaySeries(seriesToUpdate), onToggleLegendVisibility: (name, isAllSeriesSelected, isOnlyVisible) => { toggleLegendVisibility(name, isAllSeriesSelected, isOnlyVisible); } })) }))); }; const LegendOverflowType = function LegendOverflow({ className, series, onSetAllVisible, onToggleLegendVisibility, selectedSeries, onLegendItemMouseEnter, onLegendItemMouseLeave, }) { const containerRef = useRef(null); const [nonOverflowItemsNames, setNonOverflowItemsNames] = useState(series.map((s) => s.name)); useEffect(() => { setNonOverflowItemsNames(series.map((s) => s.name)); }, [series]); const overflowItemsNames = series.filter((item) => !nonOverflowItemsNames.includes(item.name)); const [width, setWidth] = useState(Infinity); useResizeObserver({ // TODO: remove type cast. use-hooks.ts 3.1.1 has a type issue with the ref, it should be HTMLElement | null // https://github.com/juliencrn/usehooks-ts/pull/680 ref: containerRef, onResize: (entry) => { if (entry.width === undefined) { return; } if (width < entry.width) { setNonOverflowItemsNames(series.map((s) => s.name)); } setWidth(entry.width); }, }); useEffect(() => { const container = containerRef.current; if (!container) { return; } let elements = Array.from(container.children); if (elements.length === 0 || series.length === 0) { return; } let totalWidth = 0; if (overflowItemsNames.length > 0) { const lastElementItem = elements[elements.length - 1]; const lastItemWidth = getComputedElementWidth(lastElementItem); elements = elements.slice(0, elements.length - 1); totalWidth += lastItemWidth; } elements.forEach((element) => { const elementWidth = getComputedElementWidth(element); totalWidth += elementWidth; const textContent = element.textContent; if (!textContent) { return; } const itemName = element.getAttribute('data-labelname'); if (itemName === null) { return; } if (totalWidth > width) { setNonOverflowItemsNames((oldNames) => oldNames.filter((name) => name !== itemName)); } }); }, [overflowItemsNames.length, series.length, width]); const classes = classNames({ 'ndl-chart-legend-calculating': width === Infinity, }, className); const selectedSeriesLength = Object.values(selectedSeries).filter(Boolean).length; const hasMoreThanOneItem = nonOverflowItemsNames.length > 1; const isAllSeriesVisible = selectedSeriesLength === series.length; const isOverflowItemsDeselected = overflowItemsNames.every((item) => { var _a; return !selectedSeries[(_a = item.name) !== null && _a !== void 0 ? _a : '']; }); return (_jsxs("div", { className: classes, ref: containerRef, children: [nonOverflowItemsNames.map((name) => { var _a; const currentSeries = series.find((s) => s.name === name); if (currentSeries === undefined) { return null; } const isDeselected = currentSeries.name === undefined ? false : !selectedSeries[currentSeries.name]; return (_jsx(LegendItem, { name: name, color: currentSeries.color, onLegendItemMouseEnter: () => !isDeselected && onLegendItemMouseEnter([currentSeries]), onLegendItemMouseLeave: () => !isDeselected && onLegendItemMouseLeave([currentSeries]), onLegendItemClick: () => { const isAllSeriesSelected = selectedSeriesLength === series.length; const isOnlyVisible = selectedSeries[name !== null && name !== void 0 ? name : ''] && selectedSeriesLength === 1; onToggleLegendVisibility === null || onToggleLegendVisibility === void 0 ? void 0 : onToggleLegendVisibility(name, isAllSeriesSelected, isOnlyVisible, series); }, hasButtons: hasMoreThanOneItem, selected: !isAllSeriesVisible && selectedSeries[(_a = currentSeries.name) !== null && _a !== void 0 ? _a : ''], deSelected: isDeselected, children: name }, name)); }), overflowItemsNames.length > 0 && (_jsxs(Tooltip, { type: "simple", children: [_jsx(Tooltip.Trigger, { hasButtonWrapper: true, children: _jsxs(LegendItem, { name: "ndl-overflow-items", color: tokens.palette.neutral[30], selected: !isAllSeriesVisible && overflowItemsNames.every((item) => { var _a; return selectedSeries[(_a = item.name) !== null && _a !== void 0 ? _a : '']; }), deSelected: isOverflowItemsDeselected, onLegendItemMouseEnter: () => !isOverflowItemsDeselected && onLegendItemMouseEnter(overflowItemsNames), onLegendItemMouseLeave: () => !isOverflowItemsDeselected && onLegendItemMouseLeave(overflowItemsNames), onLegendItemClick: () => { const selectedSeriesLength = Object.values(selectedSeries).filter(Boolean).length; const isOnlyOverflowItemsVisible = selectedSeriesLength === overflowItemsNames.length && overflowItemsNames.every((item) => { var _a; return selectedSeries[(_a = item.name) !== null && _a !== void 0 ? _a : '']; }); if (isOnlyOverflowItemsVisible) { onSetAllVisible(); return; } overflowItemsNames.forEach((item, index) => { const isAllSeriesSelected = selectedSeriesLength === series.length && index === 0; onToggleLegendVisibility === null || onToggleLegendVisibility === void 0 ? void 0 : onToggleLegendVisibility(item.name, isAllSeriesSelected, false, series); }); }, children: [overflowItemsNames.length, " more"] }) }), _jsx(Tooltip.Content, { children: overflowItemsNames.map((item) => (_jsx("p", { children: item.name }, item.name))) })] }))] })); }; //# sourceMappingURL=Legend.js.map