BarChart.tsx233 lines · main
1import { ComponentProps, useMemo, useState } from 'react'
2import {
3 Bar,
4 CartesianGrid,
5 Cell,
6 Legend,
7 BarChart as RechartBarChart,
8 Tooltip,
9 XAxis,
10 YAxis,
11} from 'recharts'
12import type { CategoricalChartState } from 'recharts/types/chart/types'
13
14import { ChartHeader } from './ChartHeader'
15import type { CommonChartProps, Datum } from './Charts.types'
16import { numberFormatter, useChartSize } from './Charts.utils'
17import NoDataPlaceholder from './NoDataPlaceholder'
18import { useChartHoverState } from './useChartHoverState'
19import { CHART_COLORS, DateTimeFormats } from '@/components/ui/Charts/Charts.constants'
20import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
21
22export interface BarChartProps<D = Datum> extends CommonChartProps<D> {
23 yAxisKey: string
24 xAxisKey: string
25 customDateFormat?: string
26 displayDateInUtc?: boolean
27 onBarClick?: (datum: D, tooltipData?: CategoricalChartState) => void
28 emptyStateMessage?: string
29 showLegend?: boolean
30 xAxisIsDate?: boolean
31 XAxisProps?: ComponentProps<typeof XAxis>
32 YAxisProps?: ComponentProps<typeof YAxis>
33 showGrid?: boolean
34 syncId?: string
35}
36
37function BarChart<D extends Datum = Datum>({
38 data,
39 yAxisKey,
40 xAxisKey,
41 format,
42 customDateFormat = DateTimeFormats.FULL,
43 title,
44 highlightedValue,
45 highlightedLabel,
46 displayDateInUtc,
47 minimalHeader,
48 valuePrecision,
49 className = '',
50 size = 'normal',
51 emptyStateMessage,
52 onBarClick,
53 showLegend = false,
54 xAxisIsDate = true,
55 XAxisProps,
56 YAxisProps,
57 showGrid = false,
58 syncId,
59}: BarChartProps<D>) {
60 const { hoveredIndex, isHovered, isCurrentChart, setHover, clearHover } =
61 useChartHoverState('default')
62 const { Container } = useChartSize(size)
63 const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
64
65 // Transform data to ensure yAxisKey values are numbers
66 const transformedData = useMemo(() => {
67 return data.map((item) => ({
68 ...item,
69 [yAxisKey]: typeof item[yAxisKey] === 'string' ? Number(item[yAxisKey]) : item[yAxisKey],
70 }))
71 }, [data, yAxisKey])
72
73 // Default props
74 const _XAxisProps = XAxisProps || {
75 interval: data.length - 2,
76 angle: 0,
77 tick: false,
78 }
79
80 const _YAxisProps = YAxisProps || {
81 tickFormatter: (value) => numberFormatter(value, valuePrecision),
82 tick: false,
83 width: 0,
84 }
85
86 // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
87 // Otherwise honour the user's selected timezone via the picker, which
88 // `useFormatDateTime` reads from context.
89 const formatPickerDate = useFormatDateTime()
90 const formatChartDate = (value: number | string) =>
91 displayDateInUtc
92 ? formatDateTime(value, { tz: 'UTC', format: customDateFormat })
93 : formatPickerDate(value, customDateFormat)
94
95 function getHeaderLabel() {
96 if (!xAxisIsDate) {
97 if (!focusDataIndex) return highlightedLabel
98 return data[focusDataIndex]?.[xAxisKey]
99 }
100 return (
101 (focusDataIndex !== null &&
102 data &&
103 data[focusDataIndex] !== undefined &&
104 formatChartDate(data[focusDataIndex][xAxisKey] as number | string)) ||
105 highlightedLabel
106 )
107 }
108
109 const resolvedHighlightedLabel = getHeaderLabel()
110
111 const resolvedHighlightedValue =
112 focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
113
114 if (data.length === 0) {
115 return (
116 <NoDataPlaceholder
117 message={emptyStateMessage}
118 description="It may take up to 24 hours for data to refresh"
119 size={size}
120 className={className}
121 attribute={title}
122 format={format}
123 />
124 )
125 }
126
127 return (
128 <div className={['flex flex-col gap-y-3', className].join(' ')}>
129 <ChartHeader
130 title={title}
131 format={format}
132 customDateFormat={customDateFormat}
133 highlightedValue={resolvedHighlightedValue}
134 highlightedLabel={resolvedHighlightedLabel}
135 minimalHeader={minimalHeader}
136 syncId={syncId}
137 data={data}
138 xAxisKey={xAxisKey}
139 yAxisKey={yAxisKey}
140 xAxisIsDate={xAxisIsDate}
141 displayDateInUtc={displayDateInUtc}
142 valuePrecision={valuePrecision}
143 attributes={[]}
144 />
145 <Container>
146 <RechartBarChart
147 data={transformedData}
148 className="overflow-visible"
149 onMouseMove={(e: any) => {
150 if (e.activeTooltipIndex !== focusDataIndex) {
151 setFocusDataIndex(e.activeTooltipIndex)
152 }
153
154 setHover(e.activeTooltipIndex)
155 }}
156 onMouseLeave={() => {
157 setFocusDataIndex(null)
158
159 clearHover()
160 }}
161 onClick={(tooltipData) => {
162 const datum = tooltipData?.activePayload?.[0]?.payload
163 if (onBarClick) onBarClick(datum, tooltipData)
164 }}
165 >
166 {showLegend && <Legend />}
167 {showGrid && <CartesianGrid stroke={CHART_COLORS.AXIS} />}
168 <YAxis
169 {..._YAxisProps}
170 axisLine={{ stroke: CHART_COLORS.AXIS }}
171 tickLine={{ stroke: CHART_COLORS.AXIS }}
172 key={yAxisKey}
173 />
174 <XAxis
175 {..._XAxisProps}
176 axisLine={{ stroke: CHART_COLORS.AXIS }}
177 tickLine={{ stroke: CHART_COLORS.AXIS }}
178 key={xAxisKey}
179 />
180 <Tooltip
181 content={(_props) =>
182 syncId && isHovered && isCurrentChart && hoveredIndex !== null ? (
183 <div className="bg-black/90 text-white p-2 rounded-sm text-xs">
184 <div className="font-medium">
185 {formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
186 </div>
187 <div>
188 {numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)}
189 {typeof format === 'string' ? format : ''}
190 </div>
191 </div>
192 ) : null
193 }
194 />
195 <Bar
196 dataKey={yAxisKey}
197 fill={CHART_COLORS.GREEN_1}
198 animationDuration={300}
199 maxBarSize={48}
200 >
201 {data?.map((_entry: D, index: number) => (
202 <Cell
203 key={`cell-${index}`}
204 className={`transition-all duration-300 ${onBarClick ? 'cursor-pointer' : ''}`}
205 fill={
206 focusDataIndex === index || focusDataIndex === null
207 ? CHART_COLORS.GREEN_1
208 : CHART_COLORS.GREEN_2
209 }
210 enableBackground={12}
211 />
212 ))}
213 </Bar>
214 </RechartBarChart>
215 </Container>
216 {data && (
217 <div className="text-foreground-lighter -mt-10 flex items-center justify-between text-[10px] font-mono">
218 <span>
219 {xAxisIsDate
220 ? formatChartDate(data[0][xAxisKey] as number | string)
221 : data[0][xAxisKey]}
222 </span>
223 <span>
224 {xAxisIsDate
225 ? formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string)
226 : data[data?.length - 1]?.[xAxisKey]}
227 </span>
228 </div>
229 )}
230 </div>
231 )
232}
233export default BarChart