AreaChart.tsx167 lines · main
| 1 | import { useState } from 'react' |
| 2 | import { Area, AreaChart as RechartAreaChart, Tooltip, XAxis } from 'recharts' |
| 3 | |
| 4 | import { ChartHeader } from './ChartHeader' |
| 5 | import type { CommonChartProps, Datum } from './Charts.types' |
| 6 | import { numberFormatter, useChartSize } from './Charts.utils' |
| 7 | import NoDataPlaceholder from './NoDataPlaceholder' |
| 8 | import { useChartHoverState } from './useChartHoverState' |
| 9 | import { CHART_COLORS, DateTimeFormats } from '@/components/ui/Charts/Charts.constants' |
| 10 | import { formatDateTime, useFormatDateTime } from '@/lib/datetime' |
| 11 | |
| 12 | export 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 | |
| 21 | const 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 | } |
| 167 | export default AreaChart |