BarChart.tsx 7.1 KB

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