Files
explore/ui/shared/chart/tooltip/ChartTooltipRow.tsx
T
2025-06-11 20:03:31 +02:00

97 lines
3.2 KiB
TypeScript

import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import type { TimeChartData } from '../types';
import type { CurrentPoint } from './ChartTooltipPoint';
import { calculateRowTransformValue, LABEL_WIDTH, PADDING } from './utils';
type Props = {
lineNum: number;
} & ({ label: string; children?: never } | { children: React.ReactNode; label?: never });
const ChartTooltipRow = ({ label, lineNum, children }: Props) => {
const labelColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white');
return (
<g className="ChartTooltip__row" transform={ calculateRowTransformValue(lineNum) }>
{ children || (
<>
<text
className="ChartTooltip__label"
transform="translate(0,0)"
dominantBaseline="hanging"
fill={ labelColor[0] }
>
{ label }
</text>
<text
className="ChartTooltip__value"
transform={ `translate(${ LABEL_WIDTH },0)` }
dominantBaseline="hanging"
fill={ textColor[0] }
/>
</>
) }
</g>
);
};
export default React.memo(ChartTooltipRow);
interface UseRenderRowsParams {
data: TimeChartData;
xScale: d3.ScaleTime<number, number>;
minWidth: number;
}
interface UseRenderRowsReturnType {
width: number;
}
export function useRenderRows(ref: React.RefObject<SVGGElement | null>, { data, xScale, minWidth }: UseRenderRowsParams) {
return React.useCallback((x: number, currentPoints: Array<CurrentPoint>): UseRenderRowsReturnType => {
// update "transform" prop of all rows
const isIncompleteData = currentPoints.some(({ item }) => item.isApproximate);
d3.select(ref.current)
.selectAll<Element, TimeChartData>('.ChartTooltip__row')
.attr('transform', (datum, index) => {
return calculateRowTransformValue(index - (isIncompleteData ? 0 : 1));
});
// update date and indicators value
// here we assume that the first value element contains the date
const valueNodes = d3.select(ref.current)
.selectAll<Element, TimeChartData>('.ChartTooltip__value')
.text((_, index) => {
if (index === 0) {
const date = xScale.invert(x);
const dateValue = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
const dateValueFallback = d3.timeFormat('%e %b %Y')(xScale.invert(x));
return dateValue || dateValueFallback;
}
const { datumIndex, item } = currentPoints.find(({ datumIndex }) => datumIndex === index - 1) || {};
if (datumIndex === undefined || !item) {
return null;
}
const value = data[datumIndex]?.valueFormatter?.(item.value) ?? item.value.toLocaleString(undefined, { minimumSignificantDigits: 1 });
const units = data[datumIndex]?.units ? ` ${ data[datumIndex]?.units }` : '';
return value + units;
})
.nodes();
const valueWidths = valueNodes.map((node) => node?.getBoundingClientRect?.().width);
const maxValueWidth = Math.max(...valueWidths);
const maxRowWidth = Math.max(minWidth, 2 * PADDING + LABEL_WIDTH + maxValueWidth);
return { width: maxRowWidth };
}, [ data, minWidth, ref, xScale ]);
}