@bluecateng/pelagos-charts
Version:
Chart components
303 lines • 11.2 kB
JavaScript
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { select } from 'd3-selection';
import identity from 'lodash-es/identity';
import { addResizeObserver, Layer, useRandomId } from '@bluecateng/pelagos';
import { axisPropType, colorPropType, dataPropType, hintPropType, legendPropType } from './ChartPropTypes';
import { getDefaultClass, getGroup } from './Getters';
import extendDomain from './extendDomain';
import getColorClass from './getColorClass';
import getColorVariant from './getColorVariant';
import getPlotBottom from './getPlotBottom';
import createScale from './createScale';
import getTicks from './getTicks';
import drawLeftAxis from './drawLeftAxis';
import drawBottomAxis from './drawBottomAxis';
import drawGrid from './drawGrid';
import hintFormatters from './hintFormatters';
import mappers from './mappers';
import tickFormatters from './tickFormatters';
import useSetHintPosition from './useSetHintPosition';
import legendDirections from './legendDirections';
import Legend from './Legend';
import drawLoadingGrid from './drawLoadingGrid';
import ChartAxes from './ChartAxes';
import LoadingGrid from './LoadingGrid';
import './Chart.less';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const {
max,
min
} = Math;
const minBarWidth = 48;
const SimpleBarChart = ({
id,
className,
data,
dataOptions,
color,
bottomAxis,
leftAxis,
legend,
hint,
getFillClass = getDefaultClass,
getBgClass = getDefaultClass,
onClick,
onLegendClick,
onSelectionChange,
...props
}) => {
id = useRandomId(id);
const {
groupFormatter: dataGroupFormatter,
groupMapper: dataGroupMapper,
loading: dataLoading,
selectedGroups: dataSelectedGroups
} = {
groupFormatter: identity,
groupMapper: getGroup,
loading: false,
...dataOptions
};
const {
groupCount: colorGroupCount,
option: colorOption
} = {
groupCount: null,
option: 1,
...(color == null ? void 0 : color.pairing)
};
const {
domain: bottomDomain,
scaleType: bottomScaleType,
title: bottomTitle
} = {
scaleType: 'labels',
...bottomAxis
};
const bottomMapper = (bottomAxis == null ? void 0 : bottomAxis.mapper) || mappers[bottomScaleType];
const {
formatter: bottomTickFormatter
} = {
formatter: tickFormatters[bottomScaleType],
...(bottomAxis == null ? void 0 : bottomAxis.ticks)
};
const {
domain: leftDomain,
scaleType: leftScaleType,
title: leftTitle
} = {
scaleType: 'linear',
...leftAxis
};
const leftMapper = (leftAxis == null ? void 0 : leftAxis.mapper) || mappers[leftScaleType];
const {
formatter: leftTickFormatter
} = {
formatter: tickFormatters[leftScaleType],
...(leftAxis == null ? void 0 : leftAxis.ticks)
};
const vertical = bottomScaleType === 'labels';
const {
alignment: legendAlignment,
clickable: legendClickable,
enabled: legendEnabled,
order: legendOrder,
position: legendPosition
} = {
alignment: 'start',
clickable: true,
enabled: false,
position: 'bottom',
...legend
};
const {
enabled: hintEnabled,
headerFormatter: hintHeaderFormatter,
valueFormatter: hintValueFormatter
} = {
enabled: true,
headerFormatter: hintFormatters[vertical ? bottomScaleType : leftScaleType],
valueFormatter: hintFormatters[vertical ? leftScaleType : bottomScaleType],
...hint
};
const gradientId = `${id}-loading`;
const state = useMemo(() => {
if (dataLoading) {
return {
groupIndex: new Map()
};
}
const getLabel = vertical ? bottomMapper : leftMapper;
const getValue = vertical ? leftMapper : bottomMapper;
const selectedSet = new Set(dataSelectedGroups);
const allSelected = selectedSet.size === 0;
const selectedData = [];
const groupIndex = new Map();
const labelSet = new Set();
let min = 0;
let max = 0;
for (const d of data) {
const group = dataGroupMapper(d);
if (!groupIndex.has(group)) {
groupIndex.set(group, groupIndex.size);
}
if (allSelected || selectedSet.has(group)) {
const label = getLabel(d);
const value = getValue(d);
selectedData.push([group, label, value]);
labelSet.add(label);
if (value < min) min = value;
if (value > max) max = value;
}
}
const valueDomain = extendDomain([min, max]);
return {
selectedData,
groupIndex,
leftDomain: leftDomain || (vertical ? valueDomain : labelSet),
bottomDomain: bottomDomain || (vertical ? labelSet : valueDomain)
};
}, [bottomDomain, bottomMapper, data, dataGroupMapper, dataLoading, dataSelectedGroups, leftDomain, leftMapper, vertical]);
const ref = useRef(null);
const drawRef = useRef(null);
const hintRef = useRef(null);
const [hintData, setHintData] = useState({});
useLayoutEffect(() => {
const {
selectedData,
groupIndex,
leftDomain,
bottomDomain
} = state;
const colorVariant = getColorVariant(colorGroupCount, groupIndex.size);
const bottomTickFormatterFn = bottomTickFormatter || identity;
const draw = drawRef.current = ({
width,
height
}) => {
const svg = ref.current;
if (dataLoading) {
drawLoadingGrid(svg, width, height);
return;
}
const nodes = svg.childNodes;
const axes = nodes[1];
const plotBottom = getPlotBottom(height, bottomTitle);
const leftScale = createScale(leftScaleType, leftDomain, [plotBottom, 0], 0.5);
const leftTickCount = height / 80;
const leftTicks = getTicks(leftScale, leftTickCount);
const plotLeft = drawLeftAxis(axes.firstChild, leftScale, leftTitle, leftTickCount, leftTicks, leftTickFormatter, plotBottom);
const bottomScale = createScale(bottomScaleType, bottomDomain, [plotLeft, width], 0.5);
const bottomTickCount = width / 80;
const bottomTicks = getTicks(bottomScale, bottomTickCount);
drawBottomAxis(axes.lastChild, bottomScale, bottomTitle, bottomTickCount, bottomTicks, bottomTickFormatter, width, height, plotBottom, plotLeft);
drawGrid(nodes[0], leftTicks, leftScale, bottomTicks, bottomScale, width, plotLeft, plotBottom);
const zero = (vertical ? leftScale : bottomScale)(0);
if (vertical) {
const needsZero = leftScaleType === 'linear' && leftDomain[0] < 0;
select(nodes[3]).selectAll('line').data(needsZero ? [1] : []).join('line').attr('x1', plotLeft).attr('x2', width).attr('y1', zero).attr('y2', zero);
} else {
const needsZero = bottomScaleType === 'linear' && bottomDomain[0] < 0;
select(nodes[3]).selectAll('line').data(needsZero ? [1] : []).join('line').attr('x1', zero).attr('x2', zero).attr('y1', 0).attr('y2', plotBottom);
}
const barWidth = min(minBarWidth, (vertical ? bottomScale : leftScale).step() / 2);
const offset = barWidth / 2;
const buildPath = vertical ? d => {
const [, d1, d2] = d;
const x = bottomScale(d1) - offset;
const size = d2 === 0 ? 0 : d2 > 0 ? min(-2, leftScale(d2) - zero) : max(2, leftScale(d2) - zero);
return `m${x},${zero}h${barWidth}v${size}h${-barWidth}z`;
} : d => {
const [, d1, d2] = d;
const y = leftScale(d1) - offset;
const size = d2 === 0 ? 0 : d2 < 0 ? min(-2, bottomScale(d2) - zero) : max(2, bottomScale(d2) - zero);
return `m${zero},${y}v${barWidth}h${size}v${-barWidth}z`;
};
const bar = select(nodes[2]).selectAll('path').data(selectedData).join('path').attr('class', d => getFillClass(d[0], d[1], d[2], getColorClass('fill', colorVariant, colorOption, groupIndex.get(d[0])))).attr('d', buildPath).attr('role', 'graphics-symbol').attr('aria-roledescription', `bar`) // TODO translate
.attr('aria-label', d => `${dataGroupFormatter(d[0]) || ''}, ${bottomTickFormatterFn(d[1])}, ${d[2]}`);
if (hintEnabled) {
bar.on('mousemove', (event, d) => setHintData({
visible: true,
x: event.clientX,
y: event.clientY,
content: /*#__PURE__*/_jsxs(Layer, {
className: "Chart__simpleHint",
children: [/*#__PURE__*/_jsx("span", {
children: hintHeaderFormatter(d[1])
}), /*#__PURE__*/_jsx("span", {
children: hintValueFormatter(d[2])
})]
})
})).on('mouseleave', () => setHintData(hintData => ({
...hintData,
visible: false
})));
} else {
bar.on('mousemove', null).on('mouseleave', null);
}
if (onClick) {
bar.on('click', (_, d) => onClick(d[1]));
} else {
bar.on('click', null);
}
};
draw(ref.current.getBoundingClientRect());
}, [bottomScaleType, bottomTickFormatter, bottomTitle, colorGroupCount, colorOption, dataGroupFormatter, dataLoading, getFillClass, hintEnabled, hintHeaderFormatter, hintValueFormatter, leftScaleType, leftTickFormatter, leftTitle, onClick, state, vertical]);
useEffect(() => addResizeObserver(ref.current, r => drawRef.current(r)), []);
useSetHintPosition(hintData, hintRef, ref);
return /*#__PURE__*/_jsxs("div", {
...props,
id: id,
className: `Chart Chart__wrapper${className ? ` ${className}` : ''}`,
children: [useMemo(() => /*#__PURE__*/_jsxs("svg", {
className: "Chart__chart",
"data-chromatic": "ignore",
ref: ref,
children: [/*#__PURE__*/_jsxs("g", {
className: "Chart__grid",
children: [/*#__PURE__*/_jsx("rect", {}), /*#__PURE__*/_jsx("g", {}), /*#__PURE__*/_jsx("g", {})]
}), /*#__PURE__*/_jsx(ChartAxes, {}), /*#__PURE__*/_jsx("g", {
className: "Chart__bars",
role: "group",
"aria-label": /* TODO translate */`data`
}), /*#__PURE__*/_jsx("g", {
className: "Chart__zero"
}), dataLoading && /*#__PURE__*/_jsx(LoadingGrid, {
gradientId: gradientId
})]
}), [dataLoading, gradientId]), legendEnabled && /*#__PURE__*/_jsx(Legend, {
className: `${legendAlignment} ${legendPosition}`,
groups: legendOrder || Array.from(state.groupIndex.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
})]
});
};
SimpleBarChart.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
data: PropTypes.any,
dataOptions: dataPropType,
color: colorPropType,
bottomAxis: axisPropType,
leftAxis: axisPropType,
legend: legendPropType,
hint: hintPropType,
getFillClass: PropTypes.func,
getBgClass: PropTypes.func,
onClick: PropTypes.func,
onLegendClick: PropTypes.func,
onSelectionChange: PropTypes.func
};
export default SimpleBarChart;