UNPKG

@neo4j-ndl/react-charts

Version:

React implementation of charts from Neo4j Design System

247 lines 16.7 kB
"use strict"; 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Legend = void 0; const jsx_runtime_1 = require("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/>. */ const base_1 = require("@neo4j-ndl/base"); const react_1 = require("@neo4j-ndl/react"); const icons_1 = require("@neo4j-ndl/react/icons"); const classnames_1 = __importDefault(require("classnames")); const echarts_1 = require("echarts"); const react_2 = require("react"); const legend_utils_1 = require("./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 = (0, classnames_1.default)('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 = (0, legend_utils_1.isThresholdLine)(name)) !== null && _b !== void 0 ? _b : false) { return null; } return ((0, jsx_runtime_1.jsxs)(Component, Object.assign({ className: classes, ref: ref, "data-labelname": name, title: name, onClick: hasButtons ? onLegendItemClick : undefined, onMouseEnter: onLegendItemMouseEnter, onMouseLeave: onLegendItemMouseLeave }, restProps, { children: [(0, jsx_runtime_1.jsx)("span", { className: "ndl-chart-legend-item-square", style: { '--ndl-chart-legend-item-color': color, backgroundColor: deSelected === true ? 'transparent' : color, }, children: selected === true && ((0, jsx_runtime_1.jsx)(icons_1.CheckIconOutline, { className: "ndl-chart-legend-item-square-checkmark" })) }), (0, jsx_runtime_1.jsx)(react_1.Typography, { variant: "body-medium", className: "ndl-chart-legend-item-text", children: children })] }))); }; 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] = (0, react_2.useState)(Object.fromEntries(series.map((s) => { var _a; return [(_a = s.name) !== null && _a !== void 0 ? _a : '', true]; }))); const highlightTimeOut = (0, react_2.useRef)(null); const downplayTimeOut = (0, react_2.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(() => { (0, legend_utils_1.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(() => { (0, legend_utils_1.highlightOrDownplaySeries)(chartRef, series, selectedSeries, seriesToUpdate, 'downplay'); }, hoverTimeOut); }; (0, react_2.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 = (0, echarts_1.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 = (0, legend_utils_1.isThresholdLine)(key)) !== null && _a !== void 0 ? _a : false); })); // Reset the series highlight to avoid series to stay blurred on selection change if (eventType === 'legendselectchanged') { (0, legend_utils_1.resetAllSeriesHighlight)(chart); } setSelectedSeries(filteredSelected); } }); }); return () => { eventTypes.forEach((eventType) => { chart === null || chart === void 0 ? void 0 : chart.off(eventType); }); }; }, [chartRef, series]); const classes = (0, classnames_1.default)(`ndl-chart-legend`, { 'ndl-chart-legend-truncation': wrappingType === 'truncation', 'ndl-chart-legend-wrapping': wrappingType === 'wrapping', }, className); const { toggleLegendVisibility, setAllVisible } = (0, legend_utils_1.useLegendVisibility)(chartRef, selectedSeries); return ((0, jsx_runtime_1.jsx)("div", Object.assign({ ref: ref, className: "ndl-chart-legend-container" }, restProps, htmlAttributes, { children: wrappingType === 'truncation' || wrappingType === 'wrapping' ? ((0, jsx_runtime_1.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 ((0, jsx_runtime_1.jsx)(react_1.ConditionalWrap, { shouldWrap: wrappingType === 'truncation', wrap: (children) => ((0, jsx_runtime_1.jsx)(react_1.Tooltip, { type: "simple", children: (0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { hasButtonWrapper: true, children: children }) }, index)), children: (0, jsx_runtime_1.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)); }) })) : ((0, jsx_runtime_1.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); } })) }))); }; exports.Legend = Legend; const LegendOverflowType = function LegendOverflow({ className, series, onSetAllVisible, onToggleLegendVisibility, selectedSeries, onLegendItemMouseEnter, onLegendItemMouseLeave, }) { const containerRef = (0, react_2.useRef)(null); const [nonOverflowItemsNames, setNonOverflowItemsNames] = (0, react_2.useState)(series.map((s) => s.name)); (0, react_2.useEffect)(() => { setNonOverflowItemsNames(series.map((s) => s.name)); }, [series]); const overflowItemsNames = series.filter((item) => !nonOverflowItemsNames.includes(item.name)); const [width, setWidth] = (0, react_2.useState)(Infinity); (0, react_1.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); }, }); (0, react_2.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 = (0, legend_utils_1.getComputedElementWidth)(lastElementItem); elements = elements.slice(0, elements.length - 1); totalWidth += lastItemWidth; } elements.forEach((element) => { const elementWidth = (0, legend_utils_1.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 = (0, classnames_1.default)({ '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 ((0, jsx_runtime_1.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 ((0, jsx_runtime_1.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 && ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { type: "simple", children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { hasButtonWrapper: true, children: (0, jsx_runtime_1.jsxs)(LegendItem, { name: "ndl-overflow-items", color: base_1.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"] }) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { children: overflowItemsNames.map((item) => ((0, jsx_runtime_1.jsx)("p", { children: item.name }, item.name))) })] }))] })); }; //# sourceMappingURL=Legend.js.map