33import { ArrowCounterClockwiseIcon , BugIcon } from "@phosphor-icons/react" ;
44import dynamic from "next/dynamic" ;
55import { useCallback , useState } from "react" ;
6- import { Area , CartesianGrid , Legend , Tooltip , XAxis , YAxis } from "recharts" ;
6+ import {
7+ Area ,
8+ CartesianGrid ,
9+ Legend ,
10+ ReferenceArea ,
11+ Tooltip ,
12+ XAxis ,
13+ YAxis ,
14+ } from "recharts" ;
715import { METRIC_COLORS , METRICS } from "@/components/charts/metrics-constants" ;
816import { Button } from "@/components/ui/button" ;
917import { ErrorChartTooltip } from "./error-chart-tooltip" ;
@@ -17,46 +25,80 @@ const AreaChart = dynamic(
1725 ( ) => import ( "recharts" ) . then ( ( mod ) => mod . AreaChart ) ,
1826 { ssr : false }
1927) ;
20- const Brush = dynamic ( ( ) => import ( "recharts" ) . then ( ( mod ) => mod . Brush ) , {
21- ssr : false ,
22- } ) ;
2328
24- interface ErrorTrendsChartProps {
29+ type ErrorTrendsChartProps = {
2530 errorChartData : Array < {
2631 date : string ;
2732 "Total Errors" : number ;
2833 "Affected Users" : number ;
2934 } > ;
30- }
35+ } ;
3136
3237export const ErrorTrendsChart = ( { errorChartData } : ErrorTrendsChartProps ) => {
33- const [ zoomDomain , setZoomDomain ] = useState < {
34- startIndex ?: number ;
35- endIndex ?: number ;
36- } > ( { } ) ;
37- const [ isZoomed , setIsZoomed ] = useState ( false ) ;
38+ const [ refAreaLeft , setRefAreaLeft ] = useState < string | null > ( null ) ;
39+ const [ refAreaRight , setRefAreaRight ] = useState < string | null > ( null ) ;
40+ const [ zoomedData , setZoomedData ] = useState < Array < {
41+ date : string ;
42+ "Total Errors" : number ;
43+ "Affected Users" : number ;
44+ } > | null > ( null ) ;
45+
46+ const isZoomed = zoomedData !== null ;
47+
48+ const displayData = zoomedData || errorChartData ;
3849
3950 const resetZoom = useCallback ( ( ) => {
40- setZoomDomain ( { } ) ;
41- setIsZoomed ( false ) ;
51+ setRefAreaLeft ( null ) ;
52+ setRefAreaRight ( null ) ;
53+ setZoomedData ( null ) ;
4254 } , [ ] ) ;
4355
44- const handleBrushChange = useCallback (
45- ( brushData : { startIndex ?: number ; endIndex ?: number } ) => {
46- if (
47- brushData &&
48- brushData . startIndex !== undefined &&
49- brushData . endIndex !== undefined
50- ) {
51- setZoomDomain ( {
52- startIndex : brushData . startIndex ,
53- endIndex : brushData . endIndex ,
54- } ) ;
55- setIsZoomed ( true ) ;
56- }
57- } ,
58- [ ]
59- ) ;
56+ const handleMouseDown = ( e : any ) => {
57+ if ( ! e ?. activeLabel ) {
58+ return ;
59+ }
60+ setRefAreaLeft ( e . activeLabel ) ;
61+ setRefAreaRight ( null ) ;
62+ } ;
63+
64+ const handleMouseMove = ( e : any ) => {
65+ if ( ! ( refAreaLeft && e ?. activeLabel ) ) {
66+ return ;
67+ }
68+ setRefAreaRight ( e . activeLabel ) ;
69+ } ;
70+
71+ const handleMouseUp = ( ) => {
72+ if ( ! refAreaLeft ) {
73+ setRefAreaLeft ( null ) ;
74+ setRefAreaRight ( null ) ;
75+ return ;
76+ }
77+
78+ const rightBoundary = refAreaRight || refAreaLeft ;
79+
80+ const leftIndex = errorChartData . findIndex ( ( d ) => d . date === refAreaLeft ) ;
81+ const rightIndex = errorChartData . findIndex (
82+ ( d ) => d . date === rightBoundary
83+ ) ;
84+
85+ if ( leftIndex === - 1 || rightIndex === - 1 ) {
86+ setRefAreaLeft ( null ) ;
87+ setRefAreaRight ( null ) ;
88+ return ;
89+ }
90+
91+ const [ startIndex , endIndex ] =
92+ leftIndex < rightIndex
93+ ? [ leftIndex , rightIndex ]
94+ : [ rightIndex , leftIndex ] ;
95+
96+ const zoomed = errorChartData . slice ( startIndex , endIndex + 1 ) ;
97+ setZoomedData ( zoomed ) ;
98+
99+ setRefAreaLeft ( null ) ;
100+ setRefAreaRight ( null ) ;
101+ } ;
60102
61103 if ( ! errorChartData . length ) {
62104 return (
@@ -94,38 +136,47 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
94136 Error occurrences and impact over time
95137 </ p >
96138 </ div >
97- { errorChartData . length > 5 && (
98- < div className = "flex items-center gap-2" >
99- { isZoomed && (
100- < Button
101- className = "h-7 px-2 text-xs"
102- onClick = { resetZoom }
103- size = "sm"
104- variant = "outline"
105- >
106- < ArrowCounterClockwiseIcon
107- className = "mr-1 h-3 w-3"
108- size = { 16 }
109- weight = "duotone"
110- />
111- Reset Zoom
112- </ Button >
113- ) }
114- < div className = "text-muted-foreground text-xs" > Drag to zoom</ div >
115- </ div >
116- ) }
139+ < div className = "flex items-center gap-2" >
140+ { isZoomed && (
141+ < Button
142+ className = "h-7 px-2 text-xs"
143+ onClick = { resetZoom }
144+ size = "sm"
145+ variant = "outline"
146+ >
147+ < ArrowCounterClockwiseIcon
148+ className = "mr-1 h-3 w-3"
149+ size = { 16 }
150+ weight = "duotone"
151+ />
152+ Reset Zoom
153+ </ Button >
154+ ) }
155+ < div className = "text-muted-foreground text-xs" > Drag to zoom</ div >
156+ </ div >
117157 </ div >
118158 < div className = "flex-1 p-2" >
119- < div style = { { width : "100%" , height : 300 } } >
159+ < div
160+ className = "relative select-none"
161+ style = { {
162+ width : "100%" ,
163+ height : 300 ,
164+ userSelect : refAreaLeft ? "none" : "auto" ,
165+ WebkitUserSelect : refAreaLeft ? "none" : "auto" ,
166+ } }
167+ >
120168 < ResponsiveContainer height = "100%" width = "100%" >
121169 < AreaChart
122- data = { errorChartData }
170+ data = { displayData }
123171 margin = { {
124172 top : 10 ,
125173 right : 10 ,
126174 left : 0 ,
127- bottom : errorChartData . length > 5 ? 35 : 5 ,
175+ bottom : displayData . length > 5 ? 35 : 5 ,
128176 } }
177+ onMouseDown = { handleMouseDown }
178+ onMouseMove = { handleMouseMove }
179+ onMouseUp = { handleMouseUp }
129180 >
130181 < defs >
131182 < linearGradient
@@ -203,9 +254,18 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
203254 wrapperStyle = { {
204255 fontSize : "10px" ,
205256 paddingTop : "5px" ,
206- bottom : errorChartData . length > 5 ? 20 : 0 ,
257+ bottom : displayData . length > 5 ? 20 : 0 ,
207258 } }
208259 />
260+ { refAreaLeft && refAreaRight && (
261+ < ReferenceArea
262+ fill = "var(--sidebar-ring)"
263+ fillOpacity = { 0.15 }
264+ strokeOpacity = { 0.3 }
265+ x1 = { refAreaLeft }
266+ x2 = { refAreaRight }
267+ />
268+ ) }
209269 < Area
210270 dataKey = "Total Errors"
211271 fill = "url(#colorTotalErrors)"
@@ -224,19 +284,6 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
224284 strokeWidth = { 2 }
225285 type = "monotone"
226286 />
227- { errorChartData . length > 5 && (
228- < Brush
229- dataKey = "date"
230- endIndex = { zoomDomain . endIndex }
231- fill = "var(--muted)"
232- fillOpacity = { 0.1 }
233- height = { 25 }
234- onChange = { handleBrushChange }
235- padding = { { top : 5 , bottom : 5 } }
236- startIndex = { zoomDomain . startIndex }
237- stroke = "var(--border)"
238- />
239- ) }
240287 </ AreaChart >
241288 </ ResponsiveContainer >
242289 </ div >
0 commit comments