@@ -14,34 +14,140 @@ import {
1414 type TeamPlayerELOLog ,
1515} from '@prisma/client' ;
1616import { BASE_ELO } from '~/utils/constants' ;
17+ import { useWindowSize } from '~/utils/hooks/use-window-size' ;
18+ import { useState } from 'react' ;
19+ import { ToggleSwitch } from '~/components/toggle-switch' ;
20+
21+ const formatSimpleDate = ( date : string ) => {
22+ return new Date ( date ) . toLocaleString ( 'no-NO' , {
23+ month : '2-digit' ,
24+ day : '2-digit' ,
25+ } ) ;
26+ } ;
27+
28+ const formatDateWithTime = ( date : string ) => {
29+ return new Date ( date ) . toLocaleString ( 'no-NO' , {
30+ year : 'numeric' ,
31+ month : '2-digit' ,
32+ day : '2-digit' ,
33+ hour : '2-digit' ,
34+ minute : '2-digit' ,
35+ } ) ;
36+ } ;
37+
38+ const formatDateOnly = ( date : string ) => {
39+ return new Date ( date ) . toLocaleString ( 'no-NO' , {
40+ year : 'numeric' ,
41+ month : '2-digit' ,
42+ day : '2-digit' ,
43+ } ) ;
44+ } ;
45+
46+ const aggregateByDay = ( data : any [ ] ) => {
47+ if ( data . length === 0 ) return [ ] ;
48+
49+ // First, group by day as before
50+ const groupedByDay = data . reduce ( ( acc , item ) => {
51+ const date = new Date ( item . date ) ;
52+ const dayKey = new Date ( date . getFullYear ( ) , date . getMonth ( ) , date . getDate ( ) )
53+ . toISOString ( )
54+ . split ( 'T' ) [ 0 ] ;
55+
56+ if ( ! acc [ dayKey ] ) {
57+ acc [ dayKey ] = { date : dayKey , elos : [ ] } ;
58+ }
59+ acc [ dayKey ] . elos . push ( item . elo ) ;
60+ return acc ;
61+ } , { } ) ;
62+
63+ // Find start date and use today as end date
64+ const dates = Object . keys ( groupedByDay ) . sort ( ) ;
65+ const startDate = new Date ( dates [ 0 ] ) ;
66+ const endDate = new Date ( ) ; // Use today's date
67+ endDate . setHours ( 23 , 59 , 59 , 999 ) ; // Set to end of today
68+
69+ // Fill in all days from start until today
70+ const filledData = [ ] ;
71+ let currentDate = new Date ( startDate ) ;
72+ let lastKnownElo = BASE_ELO ;
73+
74+ while ( currentDate <= endDate ) {
75+ const dateKey = currentDate . toISOString ( ) . split ( 'T' ) [ 0 ] ;
76+ const dayData = groupedByDay [ dateKey ] ;
77+
78+ if ( dayData ) {
79+ // Calculate average ELO for days with matches
80+ const avgElo = Math . round (
81+ dayData . elos . reduce ( ( sum : number , elo : number ) => sum + elo , 0 ) /
82+ dayData . elos . length
83+ ) ;
84+ lastKnownElo = avgElo ;
85+ filledData . push ( {
86+ date : dateKey ,
87+ elo : avgElo ,
88+ hasMatches : true ,
89+ } ) ;
90+ } else {
91+ // Use last known ELO for days without matches
92+ filledData . push ( {
93+ date : dateKey ,
94+ elo : lastKnownElo ,
95+ hasMatches : false ,
96+ } ) ;
97+ }
98+
99+ // Move to next day
100+ currentDate . setDate ( currentDate . getDate ( ) + 1 ) ;
101+ }
102+
103+ return filledData ;
104+ } ;
17105
18106interface Props {
19107 data : ELOLog [ ] | TeamELOLog [ ] | TeamPlayerELOLog [ ] ;
20108}
21109
110+ const CustomTooltip = ( { active, payload, showDailyView } : any ) => {
111+ if ( active && payload && payload . length ) {
112+ const date = payload [ 0 ] . payload . date ;
113+ const formattedDate = showDailyView
114+ ? formatDateOnly ( date )
115+ : formatDateWithTime ( date ) ;
116+
117+ return (
118+ < div className = "rounded-lg border border-gray-700 bg-gray-800/95 p-2 text-sm shadow-lg sm:p-3 sm:text-base" >
119+ < p className = "text-xs text-gray-200 sm:text-sm" > { formattedDate } </ p >
120+ < p className = "text-base font-semibold text-white sm:text-lg" >
121+ ELO: { payload [ 0 ] . value }
122+ </ p >
123+ </ div >
124+ ) ;
125+ }
126+ return null ;
127+ } ;
128+
22129export const EloHistoryChart = ( { data } : Props ) => {
130+ const [ showDailyView , setShowDailyView ] = useState ( false ) ;
131+ const { width } = useWindowSize ( ) ;
132+ const isMobile = width ? width < 640 : false ;
133+
134+ const processedData = showDailyView ? aggregateByDay ( data ) : data ;
135+
23136 const massagedData =
24- data . length > 0
137+ processedData . length > 0
25138 ? [
26139 {
27140 date : new Date (
28- new Date ( data [ 0 ] . date ) . getTime ( ) - 5 * 60000
141+ new Date ( processedData [ 0 ] . date ) . getTime ( ) - 5 * 60000
29142 ) . toISOString ( ) ,
30143 elo : BASE_ELO ,
31144 } ,
32- ...data . map ( ( dataPoint ) => ( {
33- date : dataPoint . date ,
34- elo : dataPoint . elo ,
35- } ) ) ,
145+ ...processedData ,
36146 ] . map ( ( item ) => ( {
37- date : new Date ( item . date ) . toLocaleString ( 'no-NO' , {
38- year : 'numeric' ,
39- month : '2-digit' ,
40- day : '2-digit' ,
41- hour : '2-digit' ,
42- minute : '2-digit' ,
43- } ) ,
147+ date : item . date ,
148+ displayDate : formatSimpleDate ( item . date . toString ( ) ) ,
44149 elo : item . elo ,
150+ hasMatches : 'hasMatches' in item ? item . hasMatches : false ,
45151 } ) )
46152 : [ ] ;
47153
@@ -50,21 +156,64 @@ export const EloHistoryChart = ({ data }: Props) => {
50156
51157 const yAxisDomain = [ minElo - 50 , maxElo + 50 ] ;
52158
159+ const axisColor = '#94a3b8' ;
160+
53161 return (
54- < ResponsiveContainer width = "100%" height = { 300 } >
55- < LineChart data = { massagedData } >
56- < CartesianGrid strokeDasharray = "3 3" />
57- < XAxis dataKey = "date" />
58- < YAxis domain = { yAxisDomain } />
59- < Tooltip />
60- < Legend />
61- < Line
62- type = "monotone"
63- dataKey = "elo"
64- stroke = "#8884d8"
65- activeDot = { { r : 8 } }
162+ < div className = "space-y-4" >
163+ < div className = "flex justify-end" >
164+ < ToggleSwitch
165+ checked = { showDailyView }
166+ onChange = { setShowDailyView }
167+ label = "Vis ELO-endring per dag"
66168 />
67- </ LineChart >
68- </ ResponsiveContainer >
169+ </ div >
170+ < ResponsiveContainer width = "100%" height = { isMobile ? 200 : 300 } >
171+ < LineChart data = { massagedData } >
172+ < defs >
173+ < linearGradient id = "eloColor" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
174+ < stop offset = "5%" stopColor = "#3b82f6" stopOpacity = { 0.8 } />
175+ < stop offset = "95%" stopColor = "#3b82f6" stopOpacity = { 0 } />
176+ </ linearGradient >
177+ </ defs >
178+ < CartesianGrid
179+ strokeDasharray = "3 3"
180+ stroke = "rgba(255, 255, 255, 0.1)"
181+ vertical = { false }
182+ />
183+ < XAxis
184+ dataKey = "displayDate"
185+ tick = { { fill : axisColor , fontSize : isMobile ? 10 : 12 } }
186+ tickLine = { { stroke : axisColor } }
187+ stroke = { axisColor }
188+ angle = { isMobile ? - 45 : 0 }
189+ textAnchor = { isMobile ? 'end' : 'middle' }
190+ height = { isMobile ? 60 : 30 }
191+ />
192+ < YAxis
193+ domain = { yAxisDomain }
194+ tick = { { fill : axisColor , fontSize : isMobile ? 10 : 12 } }
195+ tickLine = { { stroke : axisColor } }
196+ stroke = { axisColor }
197+ width = { isMobile ? 30 : 40 }
198+ />
199+ < Tooltip
200+ content = { ( props ) => (
201+ < CustomTooltip { ...props } showDailyView = { showDailyView } />
202+ ) }
203+ wrapperStyle = { { zIndex : 1000 } }
204+ />
205+ < Legend wrapperStyle = { { color : axisColor } } />
206+ < Line
207+ type = "monotone"
208+ dataKey = "elo"
209+ stroke = "#3b82f6"
210+ strokeWidth = { 2 }
211+ dot = { false }
212+ activeDot = { { r : 6 , fill : '#3b82f6' , stroke : '#fff' } }
213+ fill = "url(#eloColor)"
214+ />
215+ </ LineChart >
216+ </ ResponsiveContainer >
217+ </ div >
69218 ) ;
70219} ;
0 commit comments