@neo4j-ndl/react-charts
Version:
React implementation of charts from Neo4j Design System
247 lines • 16.7 kB
JavaScript
;
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