StackedBarChart.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import { useState } from 'react'
  2. import { Bar, BarChart, Cell, Legend, Tooltip, XAxis } from 'recharts'
  3. import { ChartHeader } from './ChartHeader'
  4. import {
  5. CHART_COLORS,
  6. DateTimeFormats,
  7. DEFAULT_STACK_COLORS,
  8. genStackColorScales,
  9. ValidStackColor,
  10. } from './Charts.constants'
  11. import type { CommonChartProps } from './Charts.types'
  12. import { numberFormatter, precisionFormatter, useChartSize, useStacked } from './Charts.utils'
  13. import NoDataPlaceholder from './NoDataPlaceholder'
  14. import { useChartHoverState } from './useChartHoverState'
  15. import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
  16. interface Props extends CommonChartProps<any> {
  17. xAxisKey: string
  18. yAxisKey: string
  19. stackKey: string
  20. onBarClick?: () => void
  21. variant?: 'values' | 'percentages'
  22. xAxisFormatAsDate?: boolean
  23. displayDateInUtc?: boolean
  24. hideLegend?: boolean
  25. hideHeader?: boolean
  26. stackColors?: ValidStackColor[]
  27. syncId?: string
  28. }
  29. const StackedBarChart: React.FC<Props> = ({
  30. size,
  31. data,
  32. xAxisKey,
  33. stackKey,
  34. yAxisKey,
  35. customDateFormat = DateTimeFormats.FULL,
  36. title,
  37. highlightedValue,
  38. highlightedLabel,
  39. format,
  40. minimalHeader = false,
  41. valuePrecision,
  42. onBarClick,
  43. variant,
  44. xAxisFormatAsDate = true,
  45. displayDateInUtc,
  46. hideLegend = false,
  47. hideHeader = false,
  48. stackColors = DEFAULT_STACK_COLORS,
  49. syncId,
  50. }) => {
  51. const { Container } = useChartSize(size)
  52. const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
  53. syncId || 'default'
  54. )
  55. const { dataKeys, stackedData, percentagesStackedData } = useStacked({
  56. data,
  57. xAxisKey,
  58. stackKey,
  59. yAxisKey,
  60. variant,
  61. })
  62. const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
  63. // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
  64. // Otherwise honour the user's selected timezone via the picker.
  65. const formatPickerDate = useFormatDateTime()
  66. const formatChartDate = (value: number | string) =>
  67. displayDateInUtc
  68. ? formatDateTime(value, { tz: 'UTC', format: customDateFormat })
  69. : formatPickerDate(value, customDateFormat)
  70. const resolvedHighlightedLabel =
  71. (focusDataIndex !== null &&
  72. data &&
  73. data[focusDataIndex] !== undefined &&
  74. formatChartDate(data[focusDataIndex][xAxisKey])) ||
  75. highlightedLabel
  76. const resolvedHighlightedValue =
  77. focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
  78. if (!data || data.length === 0) {
  79. return (
  80. <NoDataPlaceholder
  81. description="It may take up to 24 hours for data to refresh"
  82. size={size}
  83. attribute={title}
  84. format={format}
  85. />
  86. )
  87. }
  88. const stackColorScales = genStackColorScales(stackColors)
  89. return (
  90. <div className="w-full">
  91. {!hideHeader && (
  92. <ChartHeader
  93. title={title}
  94. format={format}
  95. customDateFormat={customDateFormat}
  96. minimalHeader={minimalHeader}
  97. highlightedValue={
  98. typeof resolvedHighlightedValue === 'number'
  99. ? numberFormatter(resolvedHighlightedValue, valuePrecision)
  100. : resolvedHighlightedValue
  101. }
  102. highlightedLabel={resolvedHighlightedLabel}
  103. syncId={syncId}
  104. data={data}
  105. xAxisKey={xAxisKey}
  106. yAxisKey={yAxisKey}
  107. xAxisIsDate={xAxisFormatAsDate}
  108. displayDateInUtc={displayDateInUtc}
  109. valuePrecision={valuePrecision}
  110. attributes={[]}
  111. />
  112. )}
  113. <Container>
  114. <BarChart
  115. data={variant === 'percentages' ? percentagesStackedData : stackedData}
  116. margin={{
  117. top: 20,
  118. right: 20,
  119. left: 20,
  120. bottom: 5,
  121. }}
  122. className="cursor-pointer overflow-visible"
  123. // mouse hover focusing logic
  124. onMouseMove={(e: any) => {
  125. if (e.activeTooltipIndex !== focusDataIndex) {
  126. setFocusDataIndex(e.activeTooltipIndex)
  127. }
  128. setHover(e.activeTooltipIndex)
  129. }}
  130. onMouseLeave={() => {
  131. setFocusDataIndex(null)
  132. clearHover()
  133. }}
  134. >
  135. {!hideLegend && (
  136. <Legend
  137. wrapperStyle={{ top: -6, fontSize: '0.8rem' }}
  138. iconSize={8}
  139. iconType="circle"
  140. verticalAlign="top"
  141. />
  142. )}
  143. <XAxis
  144. dataKey={xAxisKey}
  145. interval={data.length - 2}
  146. angle={0}
  147. tick={false}
  148. axisLine={{ stroke: CHART_COLORS.AXIS }}
  149. tickLine={{ stroke: CHART_COLORS.AXIS }}
  150. />
  151. {dataKeys.map((datum, stackIndex) => (
  152. <Bar
  153. key={stackIndex}
  154. dataKey={datum}
  155. type="monotone"
  156. legendType="circle"
  157. fill={stackColorScales[stackIndex].base}
  158. stackId={1}
  159. animationDuration={300}
  160. maxBarSize={48}
  161. className={onBarClick ? 'cursor-pointer' : ''}
  162. >
  163. {stackedData?.map((_entry: unknown, index: any) => (
  164. <Cell
  165. key={`cell-${index}`}
  166. className={`transition-all duration-300`}
  167. opacity={focusDataIndex === index ? 0.85 : 1}
  168. />
  169. ))}
  170. </Bar>
  171. ))}
  172. <Tooltip
  173. labelFormatter={
  174. xAxisFormatAsDate ? (label) => formatChartDate(label as number | string) : undefined
  175. }
  176. formatter={(value, name, props) => {
  177. const suffix = format || ''
  178. if (variant === 'percentages' && percentagesStackedData) {
  179. const index = percentagesStackedData.findIndex(
  180. (pStack) => pStack === props.payload!
  181. )
  182. const val = stackedData[index][name]
  183. const percentage = precisionFormatter(Number(value) * 100, 1) + '%'
  184. return `${percentage} (${val}${suffix})`
  185. }
  186. return String(value) + suffix
  187. }}
  188. cursor={false}
  189. labelClassName="text-white"
  190. contentStyle={{
  191. backgroundColor: '#444444',
  192. borderColor: '#444444',
  193. fontSize: '12px',
  194. }}
  195. wrapperClassName="bg-gray-600 rounded-sm min-w-md"
  196. active={!!syncId && syncTooltip && hoveredIndex !== null}
  197. />
  198. </BarChart>
  199. </Container>
  200. {stackedData && stackedData[0] && (
  201. <div className="text-foreground-lighter -mt-5 flex items-center justify-between text-xs">
  202. <span>{formatChartDate(stackedData[0][xAxisKey] as number | string)}</span>
  203. <span>
  204. {formatChartDate(stackedData[stackedData?.length - 1][xAxisKey] as number | string)}
  205. </span>
  206. </div>
  207. )}
  208. </div>
  209. )
  210. }
  211. export default StackedBarChart