import { useState, useRef, useMemo, useCallback } from 'react'; import { Bar, BarChart as RechartsBarChart, CartesianGrid, Tooltip, TooltipContentProps, XAxis, YAxis, } from 'recharts'; import { useTheme } from 'styled-components'; import { Stack } from '../../../spacing'; import { chartColors, ChartColors, fontSize } from '../../../style/theme'; import { useChartLegend } from '../legend/ChartLegendWrapper'; import { BarchartTooltip } from './BarchartTooltip'; import { formatTickValue, getTicks } from '../common/chartUtils'; import { useChartData } from './Barchart.utils'; import { ChartHeader, ChartError, ChartLoading, CustomTick, StyledResponsiveContainer, } from '../common/SharedComponents'; import { TimeType, CategoryType, UnitRange } from '../types'; const CHART_CONSTANTS = { TICK_WIDTH_OFFSET: 4, BAR_SIZE: 12, MIN_POINT_SIZE: 3, DEFAULT_HEIGHT: 200, CHART_MARGIN: { left: 0, right: -10, top: 0, bottom: 0, }, }; /* ---------------------------------- TYPE ---------------------------------- */ type BarchartDisplayOptions = { noBackground?: boolean; showHorizontalGridLines?: boolean; noYAxisLine?: boolean; noTickLine?: boolean; noHeader?: boolean; }; type ResolvedBarchartDisplayOptions = Required; const BARCHART_PRESETS: Record<'default' | 'modern', ResolvedBarchartDisplayOptions> = { default: { noBackground: false, showHorizontalGridLines: false, noYAxisLine: false, noTickLine: false, noHeader: false }, modern: { noBackground: true, showHorizontalGridLines: true, noYAxisLine: true, noTickLine: true, noHeader: true }, }; export type Point = { key: string | number; values: { label: string; value: number }[]; }; export type BarchartBars = readonly { readonly label: string; /** * When using a time type, the data should be an array of [Date, value] * so use Date instead of timestamp for transformation data in format fn */ readonly data: readonly (readonly [string | Date, number | string])[]; }[]; export type BarchartTooltipFn = (currentPoint: { category: string | number; values: { label: T[number]['label']; value: number; isHovered: boolean }[]; }) => React.ReactNode; export type BarchartSortFn = ( pointA: Record & { category: string | number }, pointB: Record & { category: string | number }, ) => 1 | -1 | 0; export type BarchartProps = { type: CategoryType | TimeType; title: string; bars?: T; tooltip?: BarchartTooltipFn; defaultSort?: BarchartSortFn; unitRange?: UnitRange; helpTooltip?: React.ReactNode; stacked?: boolean; /** * Sort the bars by default or by legend order * legend will sort the bars by the order of the colorSet property of the ChartLegendWrapper component * default will sort the bars by average values in descending order (biggest values will be at bottom) * @default 'default' */ stackedBarSort?: 'default' | 'legend'; secondaryTitle?: string; rightTitle?: React.ReactNode; height?: number; isLoading?: boolean; isError?: boolean; /** * Named display preset that sets a group of visual defaults at once. * * - `'default'` — opaque background, no grid lines, Y-axis line visible, tick marks visible. * - `'modern'` — transparent background, horizontal grid lines, no Y-axis line, no tick marks. * * Individual values can be overridden with `displayOptions`. * Defaults to `'default'` when omitted. */ displayPreset?: 'default' | 'modern'; /** * Fine-grained overrides applied on top of the active `displayPreset`. * Only the properties you specify are overridden; the rest come from the preset. */ displayOptions?: BarchartDisplayOptions; }; /* ---------------------------------- MAIN COMPONENT ---------------------------------- */ export const Barchart = (props: BarchartProps) => { const theme = useTheme(); const { getColor } = useChartLegend(); const [hoveredValue, setHoveredValue] = useState(); const chartRef = useRef(null); const { height = CHART_CONSTANTS.DEFAULT_HEIGHT, bars, type = { type: 'category' }, unitRange, stacked, stackedBarSort = 'default', defaultSort, tooltip, title, secondaryTitle, helpTooltip, rightTitle, isLoading, isError, displayPreset = 'default', displayOptions, } = props; const presetOptions = BARCHART_PRESETS[displayPreset]; const resolvedNoBackground = displayOptions?.noBackground ?? presetOptions.noBackground; const resolvedShowHorizontalGridLines = displayOptions?.showHorizontalGridLines ?? presetOptions.showHorizontalGridLines; const resolvedNoYAxisLine = displayOptions?.noYAxisLine ?? presetOptions.noYAxisLine; const resolvedNoTickLine = displayOptions?.noTickLine ?? presetOptions.noTickLine; const resolvedNoHeader = displayOptions?.noHeader ?? presetOptions.noHeader; // Create colorSet from ChartLegendWrapper const colorSet = useMemo( () => bars?.reduce( (acc, bar) => { const color = getColor(bar.label); if (color) { acc[bar.label] = color; } return acc; }, {} as Record, ), [bars, getColor], ); const { rechartsBars, unitLabel, roundReferenceValue, rechartsData, topDomain, } = useChartData( bars || [], type, colorSet || {}, stacked, defaultSort, unitRange, stackedBarSort, ); const titleWithUnit = unitLabel ? `${title} (${unitLabel})` : title; const tickFormatter = useCallback( (value: number) => formatTickValue(value, roundReferenceValue), [roundReferenceValue], ); const renderChartContent = () => { if (isError || (!bars && !isLoading)) { return ; } if (isLoading) { return ; } return ( {rechartsBars.map((bar) => { const { fill, dataKey, stackId } = bar; return ( setHoveredValue(dataKey)} onMouseLeave={() => setHoveredValue(undefined)} /> ); })} ( )} type="category" interval={0} allowDataOverflow={true} tickLine={resolvedNoTickLine ? false : { stroke: theme.border }} axisLine={{ stroke: theme.border }} /> ) => ( )} cursor={false} /> ); }; return ( {!resolvedNoHeader && ( )} {renderChartContent()} ); };