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