import React from 'react'; import {Popup} from '@gravity-ui/uikit'; import type {IHeatmapMetricValue, IHeatmapTabletData} from '../../../types/store/heatmap'; import {cn} from '../../../utils/cn'; import {formatNumber} from '../../../utils/dataFormatters/dataFormatters'; import {getColorRange, getCurrentMetricLimits} from '../util'; import i18n from './i18n'; import './Histogram.scss'; const b = cn('histogram'); const tooltipB = cn('histogram-tooltip'); interface HistogramTooltipData { count?: number; leftBound?: string; rightBound?: string; } interface HistogramTooltipContentProps { data?: HistogramTooltipData | null; } const HistogramTooltipContent = ({data}: HistogramTooltipContentProps) => { if (!data) { return null; } const {count, leftBound, rightBound} = data; return (
{i18n('label_count')} {count || '?'}
{i18n('label_from')} {leftBound || '?'}
{i18n('label_to')} {rightBound || '?'}
); }; interface HistogramBarData extends HistogramTooltipData { color?: string; } interface HistogramBarProps { data?: HistogramBarData; maxCount: number; onShowTooltip: (anchor: HTMLElement, data: HistogramTooltipData) => void; onHideTooltip: () => void; } const HistogramBar = ({data = {}, maxCount, onShowTooltip, onHideTooltip}: HistogramBarProps) => { const barRef = React.useRef(null); const {count, leftBound, rightBound, color} = data; const safeCount = Number(count) || 0; const height = maxCount > 0 ? (safeCount / maxCount) * 100 : 0; const handleMouseEnter = () => { const ref = barRef.current; if (!ref) { return; } onShowTooltip(ref, { count, leftBound, rightBound, }); }; const handleMouseLeave = () => { onHideTooltip(); }; return (
); }; interface HistogramTooltipState { anchor: HTMLElement | null; data: HistogramTooltipData | null; } export interface HistogramProps { tablets: IHeatmapTabletData[]; currentMetric?: IHeatmapMetricValue; } export const Histogram = ({tablets, currentMetric}: HistogramProps) => { const {min, max} = getCurrentMetricLimits(currentMetric, tablets); const {histogramRange, maxCount} = React.useMemo(() => { const histogramColorRange = getColorRange(50); const step = (max - min) / 50; if (step === 0 || !isFinite(step)) { // Handle edge case where all values are the same return {histogramRange: [], maxCount: 0}; } const range: HistogramBarData[] = histogramColorRange.map((item, index) => { return { color: item, count: 0, leftBound: formatNumber(min + index * step), rightBound: formatNumber(min + (index + 1) * step), }; }); let currentMaxCount = 0; tablets.forEach((tablet) => { const rawValue = currentMetric && tablet.metrics?.[currentMetric]; const value = Number(rawValue); // Skip invalid values (NaN, undefined, null, or out of range) if (!isFinite(value) || value < min || value > max) { return; } // Calculate bucket index with proper bounds checking const bucketIndex = Math.min(Math.floor((value - min) / step), range.length - 1); const currentBucket = range[bucketIndex]; const nextCountValue = (currentBucket?.count || 0) + 1; if (nextCountValue > currentMaxCount) { currentMaxCount = nextCountValue; } if (currentBucket) { range[bucketIndex] = { ...currentBucket, count: nextCountValue, }; } }); return {histogramRange: range, maxCount: currentMaxCount}; }, [tablets, currentMetric, min, max]); const [tooltipState, setTooltipState] = React.useState({ anchor: null, data: null, }); const handleShowTooltip = React.useCallback( (anchor: HTMLElement, data: HistogramTooltipData) => { setTooltipState({anchor, data}); }, [], ); const handleHideTooltip = React.useCallback(() => { setTooltipState({anchor: null, data: null}); }, []); const renderHistogramItem = (item: HistogramBarData, index: number) => { return ( ); }; return (
{tooltipState.anchor && tooltipState.data ? ( ) : null}
{Boolean(max) && histogramRange.map(renderHistogramItem)}
{formatNumber(min)}
{formatNumber(max)}
0
{formatNumber(maxCount)}
); };