UNPKG

@bluecateng/pelagos-charts

Version:
269 lines (266 loc) 9.72 kB
import { useCallback, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { sum } from 'd3-array'; import identity from 'lodash-es/identity'; import { Layer, useRandomId } from '@bluecateng/pelagos'; import CheckmarkFilled from '@carbon/icons-react/es/CheckmarkFilled'; import WarningFilled from '@carbon/icons-react/es/WarningFilled'; import ErrorFilled from '@carbon/icons-react/es/ErrorFilled'; import { colorPropType, dataPropType, hintPropType, legendPropType } from './ChartPropTypes'; import getDefaultClass from './getDefaultClass'; import legendDirections from './legendDirections'; import Legend from './Legend'; import getColorClass from './getColorClass'; import getColorVariant from './getColorVariant'; import hintFormatters from './hintFormatters'; import useSetHintPosition from './useSetHintPosition'; import './Chart.less'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const getChartValue = d => d[1]; const numberFormats = { default: new Intl.NumberFormat(undefined, {}), percent: new Intl.NumberFormat(undefined, { style: 'percent' }) }; const number = (value, style = 'default') => numberFormats[style].format(value); const formatStandardTitle = (group, value) => `${group} ${number(value, 'percent')}`; // TODO translate const defaultBreakdownFormatter = (used, available, unit) => `${number(used)} ${unit} used (${number(available)} ${unit} available)`; // TODO translate const defaultTotalFormatter = (total, unit) => `${number(total)} ${unit} total`; // TODO translate const getStateBg = (stateTotal, thresholdWarning, thresholdDanger) => stateTotal < thresholdWarning ? 'Chart__meterSuccessBg' : stateTotal < thresholdDanger ? 'Chart__meterWarningBg' : 'Chart__meterDangerBg'; const MeterChart = ({ id, className, data, dataOptions, color, meter, legend, hint, getBgClass = getDefaultClass, onClick, onLegendClick, onSelectionChange }) => { id = useRandomId(id); const { groupFormatter: dataGroupFormatter, groupMapsTo: dataGroupMapsTo, selectedGroups: dataSelectedGroups } = { groupFormatter: identity, groupMapsTo: 'group', ...dataOptions }; const { groupCount: colorGroupCount, option: colorOption } = { groupCount: null, option: 1, ...(color == null ? void 0 : color.pairing) }; const { height: meterHeight, peak: meterPeak, proportional: meterProportional, showLabels: meterShowLabels, status: meterStatus, valueMapsTo: meterValueMapsTo } = { height: 8, showLabels: true, valueMapsTo: 'value', ...meter }; const { total: proportionalTotal, unit: proportionalUnit, breakdownFormatter: proportionalBreakdownFormatter, totalFormatter: proportionalTotalFormatter } = { total: 0, unit: '', breakdownFormatter: defaultBreakdownFormatter, totalFormatter: defaultTotalFormatter, ...meterProportional }; const { warning: thresholdWarning, danger: thresholdDanger } = { ...(meterStatus == null ? void 0 : meterStatus.thresholds) }; const { alignment: legendAlignment, clickable: legendClickable, enabled: legendEnabled, order: legendOrder, position: legendPosition } = { alignment: 'start', clickable: false, enabled: !!meterProportional, position: 'bottom', ...legend }; const { enabled: hintEnabled, valueFormatter: hintValueFormatter } = { enabled: !!meterProportional, valueFormatter: hintFormatters.linear, ...hint }; const { total: stateTotal, data: stateData, groupIndex: stateGroupIndex } = useMemo(() => { const selectedSet = new Set(dataSelectedGroups); const chartData = data.map(d => [d[dataGroupMapsTo], d[meterValueMapsTo], d]); const groupIndex = new Map(chartData.map((d, index) => [d[0], index])); const filteredData = selectedSet.size === 0 ? chartData : chartData.filter(d => selectedSet.has(d[0])); return { groupIndex, data: filteredData, total: sum(filteredData, getChartValue) }; }, [data, dataGroupMapsTo, dataSelectedGroups, meterValueMapsTo]); const ref = useRef(null); const hintRef = useRef(null); const [hintData, setHintData] = useState({}); useSetHintPosition(hintData, hintRef, ref); const handleMouseMove = useCallback(event => { const datum = stateData[event.target.dataset.index]; setHintData({ visible: true, x: event.clientX, y: event.clientY, content: /*#__PURE__*/_jsxs(Layer, { className: "Chart__simpleHint", children: [/*#__PURE__*/_jsx("span", { children: dataGroupFormatter(datum[0]) }), /*#__PURE__*/_jsx("span", { children: hintValueFormatter(datum[1]) })] }) }); }, [dataGroupFormatter, hintValueFormatter, stateData]); const handleMouseLeave = useCallback(() => setHintData(hintData => ({ ...hintData, visible: false })), []); const handleClick = useCallback(event => { const datum = stateData[event.target.dataset.index]; onClick(datum[2]); }, [onClick, stateData]); const handleKeyDown = useCallback(event => { if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); event.target.click(); } }, []); const colorVariant = getColorVariant(colorGroupCount, stateGroupIndex.size); const title = !meterShowLabels ? null : meterProportional ? proportionalBreakdownFormatter(stateTotal, proportionalTotal - stateTotal, proportionalUnit) : formatStandardTitle(dataGroupFormatter(stateData[0][0]), stateData[0][1] / 100); return /*#__PURE__*/_jsxs("div", { id: id, className: `Chart Chart__wrapper${className ? ` ${className}` : ''}`, children: [/*#__PURE__*/_jsxs("div", { className: "Chart__meter", ref: ref, children: [meterShowLabels && /*#__PURE__*/_jsxs("div", { className: "Chart__meterLabels", children: [/*#__PURE__*/_jsx("div", { className: "Chart__meterTitle", title: title, children: title }), /*#__PURE__*/_jsxs("div", { className: "Chart__meterEndLabel", children: [meterProportional && /*#__PURE__*/_jsx("span", { className: "Chart__meterTotal", children: proportionalTotalFormatter(proportionalTotal, proportionalUnit) }), (meterStatus == null ? void 0 : meterStatus.thresholds) && (stateTotal < thresholdWarning ? /*#__PURE__*/_jsx(CheckmarkFilled, { className: "Chart__meterSuccess" }) : stateTotal < thresholdDanger ? /*#__PURE__*/_jsx(WarningFilled, { className: "Chart__meterWarning" }) : /*#__PURE__*/_jsx(ErrorFilled, { className: "Chart__meterDanger" }))] })] }), /*#__PURE__*/_jsxs(Layer, { className: "Chart__meterTrack", style: { height: `${meterHeight}px` }, children: [stateData.map(([group, value], index) => /*#__PURE__*/_jsx("div", { id: `${id}-${index}`, className: `Chart__meterBar ${getBgClass(group, null, value, meterProportional || !(meterStatus != null && meterStatus.thresholds) ? getColorClass('bg', colorVariant, colorOption, index) : getStateBg(stateTotal, thresholdWarning, thresholdDanger))}${onClick ? ' clickable' : ''}`, style: { width: meterProportional ? `${100 * value / proportionalTotal}%` : `${value}%` }, tabIndex: onClick ? 0 : undefined, role: onClick ? 'button' : undefined, "aria-label": onClick ? `${dataGroupFormatter(group)}, ${hintValueFormatter(value)}` : null, "data-index": index, onMouseMove: hintEnabled ? handleMouseMove : null, onMouseLeave: hintEnabled ? handleMouseLeave : null, onClick: onClick ? handleClick : null, onKeyDown: onClick ? handleKeyDown : null }, index)), Number.isFinite(meterPeak) && /*#__PURE__*/_jsx("div", { className: "Chart__meterPeak", style: { left: meterProportional ? `${100 * meterPeak / proportionalTotal}%` : `${meterPeak}%` } })] })] }), legendEnabled && /*#__PURE__*/_jsx(Legend, { className: `${legendAlignment} ${legendPosition}`, groups: legendOrder || Array.from(stateGroupIndex.keys()), formatter: dataGroupFormatter, direction: legendDirections[legendPosition], clickable: legendClickable, selected: dataSelectedGroups, color: color, getBgClass: getBgClass, onClick: onLegendClick, onChange: onSelectionChange }), /*#__PURE__*/_jsx("div", { className: `Chart__hintContainer${hintData.visible ? ' visible' : ''}`, ref: hintRef, children: hintData.content })] }); }; MeterChart.propTypes = { id: PropTypes.string, className: PropTypes.string, data: PropTypes.array, dataOptions: dataPropType, color: colorPropType, meter: PropTypes.shape({ height: PropTypes.number, peak: PropTypes.number, proportional: PropTypes.shape({ total: PropTypes.number, unit: PropTypes.string, breakdownFormatter: PropTypes.func, totalFormatter: PropTypes.func }), showLabels: PropTypes.bool, status: PropTypes.shape({ thresholds: PropTypes.shape({ warning: PropTypes.number.isRequired, danger: PropTypes.number.isRequired }) }), valueMapsTo: PropTypes.string }), legend: legendPropType, hint: hintPropType, getBgClass: PropTypes.func, onClick: PropTypes.func, onLegendClick: PropTypes.func, onSelectionChange: PropTypes.func }; export default MeterChart;