1- export default function TimeseriesChart ( ) {
1+ 'use client' ;
2+
3+ import { RiExternalLinkLine } from '@remixicon/react' ;
4+ import {
5+ BarChart ,
6+ Card ,
7+ Tab ,
8+ TabGroup ,
9+ TabList ,
10+ TabPanel ,
11+ TabPanels ,
12+ } from '@tremor/react' ;
13+ import { useFilters } from '@/hooks/useTinybirdData' ;
14+ import { useSearchParams , useRouter } from 'next/navigation' ;
15+
16+ function classNames ( ...classes : ( string | undefined | null | false ) [ ] ) {
17+ return classes . filter ( Boolean ) . join ( ' ' ) ;
18+ }
19+
20+ const valueFormatter = ( number : number ) =>
21+ Intl . NumberFormat ( 'us' ) . format ( number ) . toString ( ) ;
22+
23+ interface TimeseriesData {
24+ date : string ;
25+ category : string ; // model name
26+ total_requests : number ;
27+ total_errors : number ;
28+ total_tokens : number ;
29+ total_completion_tokens : number ;
30+ total_prompt_tokens : number ;
31+ total_cost : number ;
32+ avg_duration : number ;
33+ avg_response_time : number ;
34+ }
35+
36+ interface TimeseriesChartProps {
37+ data : {
38+ data : TimeseriesData [ ] ;
39+ } ;
40+ }
41+
42+ export default function TimeseriesChart ( { data } : TimeseriesChartProps ) {
43+ const setFilters = useFilters ( ( state ) => state . setFilters ) ;
44+ const router = useRouter ( ) ;
45+ const searchParams = useSearchParams ( ) ;
46+
47+ const dates = [ ...new Set ( data . data . map ( d => d . date ) ) ] . sort ( ) ;
48+ const models = [ ...new Set ( data . data . map ( d => d . category ) ) ] ;
49+
50+ // Create consistent color mapping with divergent colors
51+ const colorMap = {
52+ 'gpt-4' : 'orange' ,
53+ 'gpt-3.5-turbo' : 'cyan' ,
54+ 'gpt-4-turbo' : 'amber' ,
55+ 'claude-2' : 'teal' ,
56+ // Add more models as needed
57+ } ;
58+
59+ // Default colors for unknown models
60+ const defaultColors = [ 'orange' , 'cyan' , 'amber' , 'teal' , 'lime' , 'pink' ] ;
61+
62+ const transformedData = dates . map ( date => {
63+ const dayData = data . data . filter ( d => d . date === date ) ;
64+ return {
65+ date : new Date ( date ) . toLocaleDateString ( 'en-US' , {
66+ month : 'short' ,
67+ day : '2-digit'
68+ } ) ,
69+ ...models . reduce ( ( acc , model ) => ( {
70+ ...acc ,
71+ [ model ] : dayData . find ( d => d . category === model ) ?. total_cost || 0
72+ } ) , { } )
73+ } ;
74+ } ) ;
75+
76+ const tabs = [
77+ {
78+ name : 'Model' ,
79+ key : 'model' ,
80+ data : transformedData ,
81+ categories : models ,
82+ colors : models . map ( model => colorMap [ model as keyof typeof colorMap ] || defaultColors [ models . indexOf ( model ) % defaultColors . length ] ) ,
83+ summary : models . map ( model => ( {
84+ name : model ,
85+ total : data . data
86+ . filter ( d => d . category === model )
87+ . reduce ( ( sum , item ) => sum + item . total_cost , 0 ) ,
88+ color : `bg-${ colorMap [ model as keyof typeof colorMap ] || defaultColors [ models . indexOf ( model ) % defaultColors . length ] } -500` ,
89+ } ) ) ,
90+ } ,
91+ {
92+ name : 'Provider' ,
93+ key : 'provider' ,
94+ data : transformedData ,
95+ categories : models ,
96+ colors : defaultColors ,
97+ summary : models . map ( model => ( {
98+ name : model ,
99+ total : data . data
100+ . filter ( d => d . category === model )
101+ . reduce ( ( sum , item ) => sum + item . total_cost , 0 ) ,
102+ color : `bg-${ defaultColors [ models . indexOf ( model ) % defaultColors . length ] } -500` ,
103+ } ) ) ,
104+ } ,
105+ {
106+ name : 'Organization' ,
107+ key : 'organization' ,
108+ data : transformedData ,
109+ categories : models ,
110+ colors : defaultColors ,
111+ summary : models . map ( model => ( {
112+ name : model ,
113+ total : data . data
114+ . filter ( d => d . category === model )
115+ . reduce ( ( sum , item ) => sum + item . total_cost , 0 ) ,
116+ color : `bg-${ defaultColors [ models . indexOf ( model ) % defaultColors . length ] } -500` ,
117+ } ) ) ,
118+ } ,
119+ {
120+ name : 'Environment' ,
121+ key : 'environment' ,
122+ data : transformedData ,
123+ categories : models ,
124+ colors : defaultColors ,
125+ summary : models . map ( model => ( {
126+ name : model ,
127+ total : data . data
128+ . filter ( d => d . category === model )
129+ . reduce ( ( sum , item ) => sum + item . total_cost , 0 ) ,
130+ color : `bg-${ defaultColors [ models . indexOf ( model ) % defaultColors . length ] } -500` ,
131+ } ) ) ,
132+ }
133+ ] ;
134+
135+ const handleTabChange = ( index : number ) => {
136+ const tab = tabs [ index ] ;
137+ // Update URL
138+ const params = new URLSearchParams ( searchParams ) ;
139+ params . set ( 'column_name' , tab . key ) ;
140+ router . push ( `?${ params . toString ( ) } ` ) ;
141+ // Update filters which will trigger data refetch
142+ setFilters ( { column_name : tab . key } ) ;
143+ } ;
144+
2145 return (
3- < div className = "h-full" >
4- < h2 className = "text-lg font-semibold h-10 flex items-center px-4" > Usage Over Time</ h2 >
5- < div className = "h-[calc(100%-2.5rem)]" >
6- { /* Chart implementation will go here */ }
146+ < Card className = "h-full p-0" >
147+ < div className = "flex h-full flex-col" >
148+ < div className = "p-6" >
149+ < h3 className = "font-medium text-tremor-content-strong dark:text-dark-tremor-content-strong" >
150+ Requests
151+ </ h3 >
152+ < p className = "text-tremor-default text-tremor-content dark:text-dark-tremor-content" >
153+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
154+ nonumy eirmod tempor invidunt.{ ' ' }
155+ < a
156+ href = "#"
157+ className = "inline-flex items-center gap-1 text-tremor-default text-tremor-brand dark:text-dark-tremor-brand"
158+ >
159+ Learn more
160+ < RiExternalLinkLine className = "size-4" aria-hidden = { true } />
161+ </ a >
162+ </ p >
163+ </ div >
164+ < div className = "flex-1 border-t border-tremor-border p-6 dark:border-dark-tremor-border" >
165+ < TabGroup
166+ className = "h-full"
167+ onIndexChange = { handleTabChange }
168+ defaultIndex = { tabs . findIndex ( t => t . key === searchParams . get ( 'column_name' ) ) || 0 }
169+ >
170+ < div className = "md:flex md:items-center md:justify-between" >
171+ < TabList
172+ variant = "solid"
173+ className = "w-full rounded-tremor-small md:w-[400px]"
174+ >
175+ { tabs . map ( ( tab ) => (
176+ < Tab
177+ key = { tab . name }
178+ className = "w-full whitespace-nowrap px-3 justify-center ui-selected:text-tremor-content-strong ui-selected:dark:text-dark-tremor-content-strong"
179+ >
180+ { tab . name }
181+ </ Tab >
182+ ) ) }
183+ </ TabList >
184+ < div className = "hidden md:flex md:items-center md:space-x-2" >
185+ < span
186+ className = "shrink-0 animate-pulse rounded-tremor-full bg-emerald-500/30 p-1"
187+ aria-hidden = { true }
188+ >
189+ < span className = "block size-2 rounded-tremor-full bg-emerald-500" />
190+ </ span >
191+ < p className = "mt-4 text-tremor-default text-tremor-content dark:text-dark-tremor-content md:mt-0" >
192+ Updated just now
193+ </ p >
194+ </ div >
195+ </ div >
196+ < TabPanels className = "h-[calc(100%-4rem)]" >
197+ { tabs . map ( ( tab ) => (
198+ < TabPanel key = { tab . name } className = "h-full" >
199+ < ul
200+ role = "list"
201+ className = "mt-6 flex flex-wrap gap-x-20 gap-y-10"
202+ >
203+ { tab . summary . map ( ( item ) => (
204+ < li key = { item . name } >
205+ < div className = "flex items-center space-x-2" >
206+ < span
207+ className = { classNames (
208+ item . color ,
209+ 'size-3 shrink-0 rounded-sm' ,
210+ ) }
211+ aria-hidden = { true }
212+ />
213+ < p className = "font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong" >
214+ { valueFormatter ( item . total ) }
215+ </ p >
216+ </ div >
217+ < p className = "whitespace-nowrap text-tremor-default text-tremor-content dark:text-dark-tremor-content" >
218+ { item . name }
219+ </ p >
220+ </ li >
221+ ) ) }
222+ </ ul >
223+ < BarChart
224+ data = { tab . data }
225+ index = "date"
226+ categories = { tab . categories }
227+ colors = { tab . colors }
228+ stack = { true }
229+ showLegend = { false }
230+ yAxisWidth = { 45 }
231+ valueFormatter = { valueFormatter }
232+ className = "h-[calc(100%-8rem)] mt-10 hidden md:block"
233+ showTooltip = { true }
234+ showAnimation = { true }
235+ />
236+ < BarChart
237+ data = { tab . data }
238+ index = "date"
239+ categories = { tab . categories }
240+ colors = { tab . colors }
241+ stack = { true }
242+ showLegend = { false }
243+ showYAxis = { false }
244+ valueFormatter = { valueFormatter }
245+ className = "h-[calc(100%-8rem)] mt-6 md:hidden"
246+ showTooltip = { true }
247+ showAnimation = { true }
248+ />
249+ </ TabPanel >
250+ ) ) }
251+ </ TabPanels >
252+ </ TabGroup >
253+ </ div >
7254 </ div >
8- </ div >
255+ </ Card >
9256 ) ;
10- }
257+ }
0 commit comments