chart-bar.tsx226 lines · main
1'use client'
2
3import dayjs from 'dayjs'
4import { useTheme } from 'next-themes'
5import { ReactNode, useState } from 'react'
6import {
7 Bar,
8 CartesianGrid,
9 Cell,
10 BarChart as RechartBarChart,
11 ReferenceArea,
12 XAxis,
13 YAxis,
14} from 'recharts'
15import type { CategoricalChartState } from 'recharts/types/chart/types'
16import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, cn } from 'ui'
17
18const CHART_COLORS = {
19 TICK: 'hsl(var(--background-overlay-hover))',
20 AXIS: 'hsl(var(--background-overlay-hover))',
21 BRAND: 'hsl(var(--brand-default))',
22 BRAND_HOVER: 'hsl(var(--brand-500))',
23}
24
25export type ChartBarTick = {
26 timestamp: string
27 [key: string]: string | number
28}
29
30export type ChartHighlight = {
31 handleMouseDown: (e: { activeLabel?: string; coordinates?: string }) => void
32 handleMouseMove: (e: { activeLabel?: string; coordinates?: string }) => void
33 handleMouseUp: (e: { chartX?: number; chartY?: number }) => void
34 coordinates: { left?: string; right?: string }
35 clearHighlight?: () => void
36}
37
38export type ChartHighlightAction = {
39 id: string
40 label: string | ((ctx: { start: string; end: string; clear: () => void }) => string)
41 icon?: ReactNode
42 isDisabled?: (ctx: { start: string; end: string; clear: () => void }) => boolean
43 onSelect: (ctx: { start: string; end: string; clear: () => void }) => void
44}
45
46export interface ChartBarProps {
47 data: ChartBarTick[]
48 dataKey: string
49 config?: ChartConfig
50 onBarClick?: (datum: ChartBarTick, tooltipData?: CategoricalChartState) => void
51 DateTimeFormat?: string
52 isFullHeight?: boolean
53 className?: string
54 color?: string
55 hoverColor?: string
56 chartHighlight?: ChartHighlight
57 syncId?: string
58 showHighlightArea?: boolean
59 cursor?: string
60 showGrid?: boolean
61 showYAxis?: boolean
62 YAxisProps?: {
63 tick?: boolean
64 tickFormatter?: (value: any) => string
65 width?: number
66 [key: string]: any
67 }
68}
69
70export const ChartBar = ({
71 data,
72 dataKey,
73 config,
74 onBarClick,
75 DateTimeFormat = 'MMM D, YYYY, hh:mma',
76 isFullHeight = false,
77 className,
78 color = CHART_COLORS.BRAND,
79 hoverColor = CHART_COLORS.BRAND_HOVER,
80 chartHighlight,
81 syncId,
82 showHighlightArea = true,
83 cursor,
84 showGrid = false,
85 showYAxis = false,
86 YAxisProps,
87}: ChartBarProps) => {
88 const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
89 const { resolvedTheme } = useTheme()
90 const isDarkMode = resolvedTheme?.includes('dark')
91
92 if (data.length === 0) {
93 return null
94 }
95
96 const chartConfig: ChartConfig = config || {
97 [dataKey]: {
98 label: dataKey,
99 },
100 }
101
102 const showHighlightActions =
103 showHighlightArea &&
104 chartHighlight?.coordinates.left &&
105 chartHighlight?.coordinates.right &&
106 chartHighlight?.coordinates.left !== chartHighlight?.coordinates.right
107
108 const chartCursor = cursor || (chartHighlight ? 'crosshair' : 'default')
109
110 const yAxisConfig = {
111 tick: showYAxis,
112 hide: !showYAxis,
113 tickMargin: showYAxis ? (YAxisProps?.tickMargin ?? 4) : 0,
114 width: showYAxis ? (YAxisProps?.width ?? undefined) : 0,
115 axisLine: { stroke: CHART_COLORS.AXIS },
116 tickLine: { stroke: CHART_COLORS.AXIS },
117 ...YAxisProps,
118 }
119
120 const margin = {
121 top: 0,
122 right: 0,
123 left: showYAxis ? -40 : 0,
124 bottom: 0,
125 }
126
127 return (
128 <div
129 data-testid="chart-bar"
130 className={cn('flex flex-col gap-y-3 w-full', isFullHeight ? 'h-full' : 'h-24', className)}
131 >
132 <ChartContainer className="w-full! h-full" config={chartConfig}>
133 <RechartBarChart
134 data={data}
135 syncId={syncId}
136 margin={margin}
137 style={{ cursor: chartCursor }}
138 onMouseMove={(e: any) => {
139 if (e.activeTooltipIndex !== focusDataIndex) {
140 setFocusDataIndex(e.activeTooltipIndex)
141 }
142
143 if (chartHighlight) {
144 const activeTimestamp = data[e.activeTooltipIndex]?.timestamp
145 chartHighlight.handleMouseMove({
146 activeLabel: activeTimestamp?.toString(),
147 coordinates: e.activeLabel,
148 })
149 }
150 }}
151 onMouseDown={(e: any) => {
152 if (chartHighlight && e.activeTooltipIndex !== undefined) {
153 const activeTimestamp = data[e.activeTooltipIndex]?.timestamp
154 chartHighlight.handleMouseDown({
155 activeLabel: activeTimestamp?.toString(),
156 coordinates: e.activeLabel,
157 })
158 }
159 }}
160 onMouseUp={(e: any) => {
161 if (chartHighlight) {
162 chartHighlight.handleMouseUp({
163 chartX: e?.chartX,
164 chartY: e?.chartY,
165 })
166 }
167 }}
168 onMouseLeave={() => {
169 setFocusDataIndex(null)
170 if (chartHighlight?.clearHighlight) {
171 chartHighlight.clearHighlight()
172 }
173 }}
174 onClick={(tooltipData) => {
175 const datum = tooltipData?.activePayload?.[0]?.payload
176 if (onBarClick) onBarClick(datum, tooltipData)
177 }}
178 >
179 {showGrid && <CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />}
180 <YAxis {...yAxisConfig} />
181 <XAxis
182 dataKey="timestamp"
183 interval={data.length - 2}
184 tick={false}
185 axisLine={{ stroke: CHART_COLORS.AXIS }}
186 tickLine={{ stroke: CHART_COLORS.AXIS }}
187 />
188 <ChartTooltip
189 content={
190 <ChartTooltipContent
191 className="text-foreground-light -mt-5"
192 labelFormatter={(v: string) => dayjs(v).format(DateTimeFormat)}
193 />
194 }
195 />
196 {/* Selection highlight area */}
197 {showHighlightActions && (
198 <ReferenceArea
199 x1={chartHighlight?.coordinates.left}
200 x2={chartHighlight?.coordinates.right}
201 strokeOpacity={0.5}
202 stroke={isDarkMode ? '#FFFFFF' : '#0C3925'}
203 fill={isDarkMode ? '#FFFFFF' : '#0C3925'}
204 fillOpacity={0.2}
205 />
206 )}
207 <Bar dataKey={dataKey} fill={color} maxBarSize={24}>
208 {data?.map((_entry: ChartBarTick, index: number) => (
209 <Cell
210 className="cursor-pointer transition-colors"
211 key={`bar-${index}`}
212 fill={focusDataIndex === index || focusDataIndex === null ? color : hoverColor}
213 />
214 ))}
215 </Bar>
216 </RechartBarChart>
217 </ChartContainer>
218 {data && data.length > 0 && (
219 <div className="text-foreground-lighter -mt-10 flex items-center justify-between text-[10px] font-mono">
220 <span>{dayjs(data[0]['timestamp']).format(DateTimeFormat)}</span>
221 <span>{dayjs(data[data.length - 1]?.['timestamp']).format(DateTimeFormat)}</span>
222 </div>
223 )}
224 </div>
225 )
226}