@@ -14,34 +14,139 @@ 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 ) / dayData . elos . length
82+ ) ;
83+ lastKnownElo = avgElo ;
84+ filledData . push ( {
85+ date : dateKey ,
86+ elo : avgElo ,
87+ hasMatches : true ,
88+ } ) ;
89+ } else {
90+ // Use last known ELO for days without matches
91+ filledData . push ( {
92+ date : dateKey ,
93+ elo : lastKnownElo ,
94+ hasMatches : false ,
95+ } ) ;
96+ }
97+
98+ // Move to next day
99+ currentDate . setDate ( currentDate . getDate ( ) + 1 ) ;
100+ }
101+
102+ return filledData ;
103+ } ;
17104
18105interface Props {
19106 data : ELOLog [ ] | TeamELOLog [ ] | TeamPlayerELOLog [ ] ;
20107}
21108
109+ const CustomTooltip = ( { active, payload, label, showDailyView } : any ) => {
110+ if ( active && payload && payload . length ) {
111+ const date = payload [ 0 ] . payload . date ;
112+ const formattedDate = showDailyView
113+ ? formatDateOnly ( date )
114+ : formatDateWithTime ( date ) ;
115+
116+ return (
117+ < div className = "bg-gray-800/95 border border-gray-700 rounded-lg p-2 shadow-lg text-sm sm:p-3 sm:text-base" >
118+ < p className = "text-xs text-gray-200 sm:text-sm" > { formattedDate } </ p >
119+ < p className = "text-base font-semibold text-white sm:text-lg" >
120+ ELO: { payload [ 0 ] . value }
121+ </ p >
122+ </ div >
123+ ) ;
124+ }
125+ return null ;
126+ } ;
127+
22128export const EloHistoryChart = ( { data } : Props ) => {
129+ const [ showDailyView , setShowDailyView ] = useState ( false ) ;
130+ const { width } = useWindowSize ( ) ;
131+ const isMobile = width ? width < 640 : false ;
132+
133+ const processedData = showDailyView ? aggregateByDay ( data ) : data ;
134+
23135 const massagedData =
24- data . length > 0
136+ processedData . length > 0
25137 ? [
26138 {
27139 date : new Date (
28- new Date ( data [ 0 ] . date ) . getTime ( ) - 5 * 60000
140+ new Date ( processedData [ 0 ] . date ) . getTime ( ) - 5 * 60000
29141 ) . toISOString ( ) ,
30142 elo : BASE_ELO ,
31143 } ,
32- ...data . map ( ( dataPoint ) => ( {
33- date : dataPoint . date ,
34- elo : dataPoint . elo ,
35- } ) ) ,
144+ ...processedData ,
36145 ] . 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- } ) ,
146+ date : item . date ,
147+ displayDate : formatSimpleDate ( item . date . toString ( ) ) ,
44148 elo : item . elo ,
149+ hasMatches : 'hasMatches' in item ? item . hasMatches : false ,
45150 } ) )
46151 : [ ] ;
47152
@@ -50,21 +155,64 @@ export const EloHistoryChart = ({ data }: Props) => {
50155
51156 const yAxisDomain = [ minElo - 50 , maxElo + 50 ] ;
52157
158+ const axisColor = '#94a3b8' ; // Tailwind gray-400 - darker than before
159+
53160 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 } }
161+ < div className = "space-y-4" >
162+ < div className = "flex justify-end" >
163+ < ToggleSwitch
164+ checked = { showDailyView }
165+ onChange = { setShowDailyView }
166+ label = "Vis ELO-endring per dag"
66167 />
67- </ LineChart >
68- </ ResponsiveContainer >
168+ </ div >
169+ < ResponsiveContainer width = "100%" height = { isMobile ? 200 : 300 } >
170+ < LineChart data = { massagedData } >
171+ < defs >
172+ < linearGradient id = "eloColor" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
173+ < stop offset = "5%" stopColor = "#3b82f6" stopOpacity = { 0.8 } />
174+ < stop offset = "95%" stopColor = "#3b82f6" stopOpacity = { 0 } />
175+ </ linearGradient >
176+ </ defs >
177+ < CartesianGrid
178+ strokeDasharray = "3 3"
179+ stroke = "rgba(255, 255, 255, 0.1)"
180+ vertical = { false }
181+ />
182+ < XAxis
183+ dataKey = "displayDate"
184+ tick = { { fill : axisColor , fontSize : isMobile ? 10 : 12 } }
185+ tickLine = { { stroke : axisColor } }
186+ stroke = { axisColor }
187+ angle = { isMobile ? - 45 : 0 }
188+ textAnchor = { isMobile ? 'end' : 'middle' }
189+ height = { isMobile ? 60 : 30 }
190+ />
191+ < YAxis
192+ domain = { yAxisDomain }
193+ tick = { { fill : axisColor , fontSize : isMobile ? 10 : 12 } }
194+ tickLine = { { stroke : axisColor } }
195+ stroke = { axisColor }
196+ width = { isMobile ? 30 : 40 }
197+ />
198+ < Tooltip
199+ content = { ( props ) => < CustomTooltip { ...props } showDailyView = { showDailyView } /> }
200+ wrapperStyle = { { zIndex : 1000 } }
201+ />
202+ < Legend
203+ wrapperStyle = { { color : axisColor } }
204+ />
205+ < Line
206+ type = "monotone"
207+ dataKey = "elo"
208+ stroke = "#3b82f6"
209+ strokeWidth = { 2 }
210+ dot = { false }
211+ activeDot = { { r : 6 , fill : '#3b82f6' , stroke : '#fff' } }
212+ fill = "url(#eloColor)"
213+ />
214+ </ LineChart >
215+ </ ResponsiveContainer >
216+ </ div >
69217 ) ;
70218} ;
0 commit comments