@@ -14,34 +14,140 @@ import {
14
14
type TeamPlayerELOLog ,
15
15
} from '@prisma/client' ;
16
16
import { 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
+ } ;
17
105
18
106
interface Props {
19
107
data : ELOLog [ ] | TeamELOLog [ ] | TeamPlayerELOLog [ ] ;
20
108
}
21
109
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
+
22
129
export 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
+
23
136
const massagedData =
24
- data . length > 0
137
+ processedData . length > 0
25
138
? [
26
139
{
27
140
date : new Date (
28
- new Date ( data [ 0 ] . date ) . getTime ( ) - 5 * 60000
141
+ new Date ( processedData [ 0 ] . date ) . getTime ( ) - 5 * 60000
29
142
) . toISOString ( ) ,
30
143
elo : BASE_ELO ,
31
144
} ,
32
- ...data . map ( ( dataPoint ) => ( {
33
- date : dataPoint . date ,
34
- elo : dataPoint . elo ,
35
- } ) ) ,
145
+ ...processedData ,
36
146
] . 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 ( ) ) ,
44
149
elo : item . elo ,
150
+ hasMatches : 'hasMatches' in item ? item . hasMatches : false ,
45
151
} ) )
46
152
: [ ] ;
47
153
@@ -50,21 +156,64 @@ export const EloHistoryChart = ({ data }: Props) => {
50
156
51
157
const yAxisDomain = [ minElo - 50 , maxElo + 50 ] ;
52
158
159
+ const axisColor = '#94a3b8' ;
160
+
53
161
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"
66
168
/>
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 >
69
218
) ;
70
219
} ;
0 commit comments