AreaChart.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { useState } from 'react'
  2. import { Area, AreaChart as RechartAreaChart, Tooltip, XAxis } from 'recharts'
  3. import { ChartHeader } from './ChartHeader'
  4. import type { CommonChartProps, Datum } from './Charts.types'
  5. import { numberFormatter, useChartSize } from './Charts.utils'
  6. import NoDataPlaceholder from './NoDataPlaceholder'
  7. import { useChartHoverState } from './useChartHoverState'
  8. import { CHART_COLORS, DateTimeFormats } from '@/components/ui/Charts/Charts.constants'
  9. import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
  10. export interface AreaChartProps<D = Datum> extends CommonChartProps<D> {
  11. yAxisKey: string
  12. xAxisKey: string
  13. format?: string
  14. customDateFormat?: string
  15. displayDateInUtc?: boolean
  16. syncId?: string
  17. }
  18. const AreaChart = ({
  19. data,
  20. yAxisKey,
  21. xAxisKey,
  22. format,
  23. customDateFormat = DateTimeFormats.FULL,
  24. title,
  25. highlightedValue,
  26. highlightedLabel,
  27. displayDateInUtc,
  28. minimalHeader,
  29. className = '',
  30. valuePrecision,
  31. size = 'normal',
  32. syncId,
  33. }: AreaChartProps) => {
  34. const { Container } = useChartSize(size)
  35. const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
  36. syncId || 'default'
  37. )
  38. const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
  39. // When `displayDateInUtc` is set the chart explicitly wants UTC labels (used
  40. // by views that display server time). Otherwise honour the user's selected
  41. // timezone via the picker, which `useFormatDateTime` reads from context.
  42. const formatPickerDate = useFormatDateTime()
  43. const formatChartDate = (value: number | string) =>
  44. displayDateInUtc
  45. ? formatDateTime(value, { tz: 'UTC', format: customDateFormat })
  46. : formatPickerDate(value, customDateFormat)
  47. const resolvedHighlightedLabel =
  48. (focusDataIndex !== null &&
  49. data &&
  50. data[focusDataIndex] !== undefined &&
  51. formatChartDate(data[focusDataIndex][xAxisKey])) ||
  52. highlightedLabel
  53. const resolvedHighlightedValue =
  54. focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
  55. if (data.length === 0) {
  56. return (
  57. <NoDataPlaceholder
  58. description="It may take up to 24 hours for data to refresh"
  59. size={size}
  60. className={className}
  61. attribute={title}
  62. format={format}
  63. />
  64. )
  65. }
  66. return (
  67. <div className={['flex flex-col gap-3', className].join(' ')}>
  68. <ChartHeader
  69. title={title}
  70. format={format}
  71. customDateFormat={customDateFormat}
  72. highlightedValue={
  73. typeof resolvedHighlightedValue === 'number'
  74. ? numberFormatter(resolvedHighlightedValue, valuePrecision)
  75. : resolvedHighlightedValue
  76. }
  77. highlightedLabel={resolvedHighlightedLabel}
  78. minimalHeader={minimalHeader}
  79. syncId={syncId}
  80. data={data}
  81. xAxisKey={xAxisKey}
  82. yAxisKey={yAxisKey}
  83. xAxisIsDate={true}
  84. displayDateInUtc={displayDateInUtc}
  85. valuePrecision={valuePrecision}
  86. attributes={[]}
  87. />
  88. <Container>
  89. <RechartAreaChart
  90. data={data}
  91. margin={{
  92. top: 0,
  93. right: 0,
  94. left: 0,
  95. bottom: 0,
  96. }}
  97. className="overflow-visible"
  98. onMouseMove={(e: any) => {
  99. if (e.activeTooltipIndex !== focusDataIndex) {
  100. setFocusDataIndex(e.activeTooltipIndex)
  101. }
  102. setHover(e.activeTooltipIndex)
  103. }}
  104. onMouseLeave={() => {
  105. setFocusDataIndex(null)
  106. clearHover()
  107. }}
  108. >
  109. <defs>
  110. <linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
  111. <stop offset="5%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0.8} />
  112. <stop offset="95%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0} />
  113. </linearGradient>
  114. </defs>
  115. <XAxis
  116. dataKey={xAxisKey}
  117. interval={data.length - 2}
  118. angle={0}
  119. // hide the tick
  120. tick={false}
  121. // color the axis
  122. axisLine={{ stroke: CHART_COLORS.AXIS }}
  123. tickLine={{ stroke: CHART_COLORS.AXIS }}
  124. />
  125. <Tooltip
  126. content={(_props) =>
  127. syncId && syncTooltip && hoveredIndex !== null ? (
  128. <div className="bg-black/90 text-white p-2 rounded-sm text-xs">
  129. <div className="font-medium">
  130. {formatChartDate(data[hoveredIndex]?.[xAxisKey] as number | string)}
  131. </div>
  132. <div>
  133. {numberFormatter(Number(data[hoveredIndex]?.[yAxisKey]) || 0, valuePrecision)}
  134. {format}
  135. </div>
  136. </div>
  137. ) : null
  138. }
  139. />
  140. <Area
  141. type="monotone"
  142. dataKey={yAxisKey}
  143. stroke={CHART_COLORS.GREEN_1}
  144. fillOpacity={1}
  145. fill="url(#colorUv)"
  146. />
  147. </RechartAreaChart>
  148. </Container>
  149. {data && (
  150. <div className="text-foreground-lighter -mt-8 flex items-center justify-between text-xs">
  151. <span>{formatChartDate(data[0][xAxisKey] as number | string)}</span>
  152. <span>{formatChartDate(data[data?.length - 1]?.[xAxisKey] as number | string)}</span>
  153. </div>
  154. )}
  155. </div>
  156. )
  157. }
  158. export default AreaChart