11"use client" ;
22
3- import { ResponsiveLine } from "@nivo/line" ;
43import { DateTime } from "luxon" ;
5- import { Tilt_Warp } from "next/font/google" ;
64import Link from "next/link" ;
5+ import { useMemo } from "react" ;
6+
77import { useGetBotTimeSeries } from "../../../../api/analytics/hooks/bots/useGetBotTimeSeries" ;
88import { BucketSelection } from "../../../../components/BucketSelection" ;
99import { ChartTooltip } from "../../../../components/charts/ChartTooltip" ;
10+ import { TimeSeriesChart } from "../../../../components/charts/TimeSeriesChart" ;
11+ import type { TimeSeriesChartPoint } from "../../../../components/charts/TimeSeriesChart" ;
12+ import { getChartTimeBounds } from "../../../../components/charts/timeSeriesChartUtils" ;
1013import { RybbitTextLogo } from "../../../../components/RybbitLogo" ;
1114import { Card , CardContent , CardLoader } from "../../../../components/ui/card" ;
1215import { Skeleton } from "../../../../components/ui/skeleton" ;
1316import { useWhiteLabel } from "../../../../hooks/useIsWhiteLabel" ;
1417import { authClient } from "../../../../lib/auth" ;
15- import { formatChartDateTime , hour12 , userLocale } from "../../../../lib/dateTimeUtils" ;
16- import { useNivoTheme } from "../../../../lib/nivo" ;
18+ import { formatChartDateTime } from "../../../../lib/dateTimeUtils" ;
1719import { getTimezone , useStore } from "../../../../lib/store" ;
1820
19- const tilt_wrap = Tilt_Warp ( {
20- subsets : [ "latin" ] ,
21- weight : "400" ,
22- } ) ;
21+ type BotPoint = TimeSeriesChartPoint & {
22+ currentTime : DateTime ;
23+ } ;
2324
2425export function BotChart ( ) {
2526 const session = authClient . useSession ( ) ;
26- const { site, bucket } = useStore ( ) ;
27+ const { site, bucket, time } = useStore ( ) ;
2728 const timezone = getTimezone ( ) ;
28- const nivoTheme = useNivoTheme ( ) ;
2929 const { isWhiteLabel } = useWhiteLabel ( ) ;
3030
3131 const { data : timeSeriesData , isLoading, isFetching } = useGetBotTimeSeries ( { site } ) ;
3232
33- const processedData =
34- timeSeriesData ?. data
35- ?. map ( item => {
36- const timestamp = DateTime . fromSQL ( item . time , { zone : timezone } ) . toUTC ( ) ;
37- if ( timestamp > DateTime . now ( ) ) return null ;
38- return {
39- time : timestamp . toFormat ( "yyyy-MM-dd HH:mm:ss" ) ,
40- bot_requests : item . bot_requests ,
41- } ;
42- } )
43- . filter ( item => item !== null ) ?? [ ] ;
33+ const { current, chartMin, chartMax, max } = useMemo ( ( ) => {
34+ const { min : boundsMin , max : boundsMax } = getChartTimeBounds ( time , bucket , timezone ) ;
35+
36+ const now = DateTime . now ( ) ;
37+ const lowerBoundMs = boundsMin ?. getTime ( ) ;
38+ const upperBoundMs = ( boundsMax ?? now . toJSDate ( ) ) . getTime ( ) ;
39+ const points : BotPoint [ ] = [ ] ;
4440
45- const data = [
46- {
47- id : "Bot requests" ,
48- color : "hsl(var(--red-400))" ,
49- data : processedData . map ( item => ( {
50- x : item . time ,
41+ timeSeriesData ?. data ?. forEach ( item => {
42+ const timestamp = DateTime . fromSQL ( item . time , { zone : timezone } ) . toUTC ( ) ;
43+ if ( timestamp > now ) return ;
44+ const timestampMs = timestamp . toMillis ( ) ;
45+ if ( lowerBoundMs !== undefined && timestampMs < lowerBoundMs ) return ;
46+ if ( timestampMs > upperBoundMs ) return ;
47+ points . push ( {
48+ x : timestamp . toJSDate ( ) ,
5149 y : item . bot_requests ,
52- } ) ) ,
53- } ,
54- ] . filter ( series => series . data . length > 0 ) ;
50+ currentTime : timestamp ,
51+ } ) ;
52+ } ) ;
5553
56- const formatXAxisValue = ( value : any ) => {
57- const dt = DateTime . fromJSDate ( value , { zone : "utc" } ) . setZone ( timezone ) . setLocale ( userLocale ) ;
58- if (
59- bucket === "hour" ||
60- bucket === "minute" ||
61- bucket === "five_minutes" ||
62- bucket === "ten_minutes" ||
63- bucket === "fifteen_minutes"
64- ) {
65- return dt . toFormat ( hour12 ? "ha" : "HH:mm" ) ;
66- }
67- return dt . toFormat ( hour12 ? "MMM d" : "dd MMM" ) ;
68- } ;
54+ const dataMin = points . length ? points [ 0 ] . x : undefined ;
55+ const dataMax = points . length ? points [ points . length - 1 ] . x : undefined ;
56+
57+ return {
58+ current : points ,
59+ chartMin : boundsMin ?? dataMin ,
60+ chartMax : boundsMax ?? dataMax ?? now . toJSDate ( ) ,
61+ max : points . reduce ( ( largest , point ) => Math . max ( largest , point . y ) , 0 ) ,
62+ } ;
63+ } , [ bucket , time , timeSeriesData , timezone ] ) ;
6964
7065 return (
7166 < Card className = "overflow-visible" >
@@ -90,7 +85,7 @@ export function BotChart() {
9085 < div className = "space-y-3" >
9186 < Skeleton className = "w-full h-[300px] rounded-md" />
9287 </ div >
93- ) : data . length === 0 ? (
88+ ) : current . length === 0 ? (
9489 < div className = "h-[300px] w-full flex items-center justify-center text-neutral-500" >
9590 < div className = "text-center" >
9691 < p className = "text-lg font-medium" > No bot data available</ p >
@@ -99,72 +94,27 @@ export function BotChart() {
9994 </ div >
10095 ) : (
10196 < div className = "h-[300px] w-full" >
102- < ResponsiveLine
103- data = { data }
104- theme = { nivoTheme }
105- margin = { { top : 10 , right : 20 , bottom : 30 , left : 40 } }
106- xScale = { {
107- type : "time" ,
108- format : "%Y-%m-%d %H:%M:%S" ,
109- precision : "second" ,
110- useUTC : true ,
111- } }
112- yScale = { {
113- type : "linear" ,
114- min : 0 ,
115- stacked : false ,
116- reverse : false ,
117- } }
118- enableGridX = { true }
119- enableGridY = { true }
120- gridYValues = { 5 }
121- axisTop = { null }
122- axisRight = { null }
123- axisBottom = { {
124- tickSize : 5 ,
125- tickPadding : 10 ,
126- tickRotation : 0 ,
127- truncateTickAt : 0 ,
128- format : formatXAxisValue ,
129- } }
130- axisLeft = { {
131- tickSize : 5 ,
132- tickPadding : 10 ,
133- tickRotation : 0 ,
134- truncateTickAt : 0 ,
135- tickValues : 5 ,
136- format : value => Number ( value ) . toLocaleString ( ) ,
137- } }
138- colors = { d => d . color }
139- enableTouchCrosshair = { true }
140- enablePoints = { false }
141- useMesh = { true }
142- animate = { false }
143- enableSlices = "x"
144- enableArea = { true }
145- lineWidth = { 1 }
146- sliceTooltip = { ( { slice } : any ) => {
147- const currentTime = DateTime . fromJSDate ( new Date ( slice . points [ 0 ] . data . x ) , { zone : "utc" } ) . setZone (
148- timezone
149- ) ;
150-
151- return (
152- < ChartTooltip >
153- < div className = "p-3 min-w-[150px]" >
154- < div className = "mb-2" > { formatChartDateTime ( currentTime , bucket ) } </ div >
155- { slice . points . map ( ( point : any ) => (
156- < div key = { point . seriesId } className = "flex justify-between items-center gap-4" >
157- < div className = "flex items-center gap-2" >
158- < div className = "w-1 h-3 rounded-[3px]" style = { { backgroundColor : point . seriesColor } } />
159- < span > { point . seriesId } </ span >
160- </ div >
161- < span className = "font-medium" > { Number ( point . data . yFormatted ) . toLocaleString ( ) } </ span >
162- </ div >
163- ) ) }
97+ < TimeSeriesChart
98+ current = { current }
99+ max = { max }
100+ chartMin = { chartMin }
101+ chartMax = { chartMax }
102+ currentColor = "hsl(var(--red-400))"
103+ yTickFormat = { value => Number ( value ) . toLocaleString ( ) }
104+ renderTooltip = { ( { point, bucket } ) => (
105+ < ChartTooltip >
106+ < div className = "p-3 min-w-[150px]" >
107+ < div className = "mb-2" > { formatChartDateTime ( point . currentTime , bucket ) } </ div >
108+ < div className = "flex justify-between items-center gap-4" >
109+ < div className = "flex items-center gap-2" >
110+ < div className = "w-1 h-3 rounded-[3px]" style = { { backgroundColor : "hsl(var(--red-400))" } } />
111+ < span > Bot requests</ span >
112+ </ div >
113+ < span className = "font-medium" > { point . y . toLocaleString ( ) } </ span >
164114 </ div >
165- </ ChartTooltip >
166- ) ;
167- } }
115+ </ div >
116+ </ ChartTooltip >
117+ ) }
168118 />
169119 </ div >
170120 ) }
0 commit comments