import { createContext, useCallback, useContext, useMemo } from 'react'; import { Bar, BarChart, type BarProps, CartesianGrid, Cell, Label, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import { TOOLTIP_STYLE, COUNT_STYLE, LABEL_STYLE, MAX_TICK_LABEL_CHARS, TITLE_STYLE, TICKS_SHOW_ALL_LABELS_BELOW, UNITS_LABEL_OFFSET, TICK_MARGIN, COUNT_KEY, } from '../../constants/chartConstants'; import { BarCountFillMode, BaseBarChartProps, CategoricalChartDataItem, CustomBarLabelProps, TooltipPayload, } from '../../types/chartTypes'; import { useChartTranslation } from '../../ChartConfigProvider'; import NoData from '../NoData'; import { useTransformedChartData } from '../../util/chartUtils'; import ChartWrapper from './ChartWrapper'; const tickFormatter = (tickLabel: string) => { if (tickLabel.length <= MAX_TICK_LABEL_CHARS) { return tickLabel; } return `${tickLabel.substring(0, MAX_TICK_LABEL_CHARS)}...`; }; const BAR_CHART_MARGINS = { bottom: 100, right: 0 }; const BAR_CHART_MARGIN_TOP_COUNTS = 35; const BAR_CHART_MARGIN_TOP_NO_COUNTS = 10; const MIN_BAR_WIDTH_FOR_COUNTS = 11; const BAR_LABEL_SPACING = 4; // Spacing of a BarLabel above the actual bar, in pixels. const BAR_LABEL_APPROX_NUMBER_WIDTH = 9.2; const BaseBarChart = ({ height, width, units, title, onClick, onChartClick, chartFill, otherFill, showBarCounts, barCountFillMode, ...params }: BaseBarChartProps) => { showBarCounts = showBarCounts ?? true; // Show bar counts by default const t = useChartTranslation(); const margins = useMemo( () => ({ ...BAR_CHART_MARGINS, // Top margin needs to accommodate bar count labels: top: showBarCounts ? BAR_CHART_MARGIN_TOP_COUNTS : BAR_CHART_MARGIN_TOP_NO_COUNTS, }), [showBarCounts] ); const fill = (entry: CategoricalChartDataItem, index: number) => entry.x === 'missing' ? otherFill : chartFill[index % chartFill.length]; const data = useTransformedChartData(params, true); const totalCount = data.reduce((sum, e) => sum + e.y, 0); const onHover: BarProps['onMouseEnter'] = useCallback( (_data, _index, e) => { const { target } = e; if (onClick && target) (target as SVGElement).style.cursor = 'pointer'; }, [onClick] ); if (data.length === 0) { return ; } // string length of widest label const valuesMaxStringLength = Math.max(...data.map((d) => (d.y ?? 0).toString().length)); // Regarding XAxis.ticks below: // The weird conditional is added from https://github.com/recharts/recharts/issues/2593#issuecomment-1311678397 // Basically, if data is empty, Recharts will default to a domain of [0, "auto"] and our tickFormatter trips up // on formatting a non-string. This hack manually overrides the ticks for the axis and blanks it out. // - David L, 2023-01-03 return (
{title}
} /> : undefined} > {data.map((entry, index) => ( ))}
); }; const BarTooltip = ({ active, payload, totalCount, }: { active?: boolean; payload?: TooltipPayload; totalCount: number; }) => { if (!active) { return null; } const name = (payload && payload[0]?.payload?.x) || ''; const value = (payload && payload[0]?.value) || 0; const percentage = totalCount ? Math.round((value / totalCount) * 100) : 0; return (

{name}

{value} ({percentage}%)

); }; const BarLabelContext = createContext<{ barCountFillMode: BarCountFillMode }>({ barCountFillMode: 'neutral' }); /** * Component for rendering bar counts directly above bars in the plot. */ const BarLabel = ({ x, y, width, value, fill, valuesMaxStringLength }: CustomBarLabelProps) => { const { barCountFillMode } = useContext(BarLabelContext); // Funky conversion to placate TypeScript. In reality, width should always be a number here, the Recharts types are // just incorrect or something. // noinspection SuspiciousTypeOfGuard const finalWidth = typeof width === 'string' ? MIN_BAR_WIDTH_FOR_COUNTS : (width ?? 0); // Flip the labels to vertical text when the bar width is (roughly) less than the width of widest count // noinspection SuspiciousTypeOfGuard const vertical = finalWidth < valuesMaxStringLength * BAR_LABEL_APPROX_NUMBER_WIDTH; // noinspection SuspiciousTypeOfGuard const xPos = typeof x === 'number' ? x + finalWidth / 2 : x; return ( {/* Hide 0-count values to avoid a bunch of "0" spam in histograms with empty bars */} {finalWidth < MIN_BAR_WIDTH_FOR_COUNTS || value === 0 ? '' : value} ); }; export default BaseBarChart;