@@ -210,45 +210,105 @@ export async function attachHourlyAverages(
210210 penalty : allValidMatches . length > 0 ? Number ( ( allValidMatches . reduce ( ( sum , row ) => sum + ( row . penalty ?? 0 ) , 0 ) / allValidMatches . length ) . toFixed ( 2 ) ) : 5 ,
211211 } ;
212212
213- // Return matches with average data attached (but keep original match data intact)
214- return matches . map ( ( match ) => {
215- const window = getHourWindow ( match . date ) ;
216- let avg = hourlyAverages . get ( `${ window . start } -${ window . end } ` ) ;
213+ // Helper function to get smooth average using expanding time window
214+ const getSmoothAverage = ( matchDate : string , previousAverage ?: { points : number ; tele : number ; penalty : number } ) => {
215+ const matchTime = new Date ( matchDate ) . getTime ( ) ;
217216
218- // If no exact hour match, find the closest hour with actual data
219- if ( ! avg ) {
220- const matchTime = new Date ( match . date ) . getTime ( ) ;
221- let closestAvg = null ;
222- let closestDistance = Infinity ;
217+ // Try expanding time windows until we find enough data
218+ const windowSizes = [ 1 , 2 , 4 , 8 , 12 , 24 ] ; // hours
219+
220+ for ( const windowSize of windowSizes ) {
221+ const startTime = new Date ( matchTime - ( windowSize * 60 * 60 * 1000 ) ) ;
222+ const endTime = new Date ( matchTime + ( windowSize * 60 * 60 * 1000 ) ) ;
223+
224+ const relevantMatches = data . filter ( row => {
225+ const rowTime = new Date ( row . date ) . getTime ( ) ;
226+ return rowTime >= startTime . getTime ( ) &&
227+ rowTime <= endTime . getTime ( ) &&
228+ ( row . totalPoints ?? 0 ) > 0 ;
229+ } ) ;
230+
231+ // Reduce minimum matches required for smaller windows to make it more responsive
232+ const minMatches = windowSize <= 2 ? 3 : windowSize <= 8 ? 5 : 8 ;
223233
224- // Find the closest hour with actual data
225- for ( const [ windowKey , avgData ] of hourlyAverages ) {
226- const windowStart = windowKey . split ( '-' ) [ 0 ] ;
227- try {
228- const windowTime = new Date ( windowStart ) . getTime ( ) ;
229- const distance = Math . abs ( windowTime - matchTime ) ;
230-
231- if ( distance < closestDistance ) {
232- closestDistance = distance ;
233- closestAvg = avgData ;
234- }
235- } catch ( e ) {
236- continue ; // Skip invalid dates
234+ if ( relevantMatches . length >= minMatches ) {
235+ let totalPoints = 0 ;
236+ let totalTele = 0 ;
237+ let totalPenalty = 0 ;
238+
239+ for ( const row of relevantMatches ) {
240+ totalPoints += row . totalPoints ?? 0 ;
241+ totalTele += row . tele ?? 0 ;
242+ totalPenalty += row . penalty ?? 0 ;
237243 }
244+
245+ const newAverage = {
246+ points : Number ( ( totalPoints / relevantMatches . length ) . toFixed ( 2 ) ) ,
247+ tele : Number ( ( totalTele / relevantMatches . length ) . toFixed ( 2 ) ) ,
248+ penalty : Number ( ( totalPenalty / relevantMatches . length ) . toFixed ( 2 ) ) ,
249+ } ;
250+
251+ // If we have a previous average, smooth the transition (blend 70% new, 30% previous)
252+ if ( previousAverage ) {
253+ return {
254+ points : Number ( ( 0.7 * newAverage . points + 0.3 * previousAverage . points ) . toFixed ( 2 ) ) ,
255+ tele : Number ( ( 0.7 * newAverage . tele + 0.3 * previousAverage . tele ) . toFixed ( 2 ) ) ,
256+ penalty : Number ( ( 0.7 * newAverage . penalty + 0.3 * previousAverage . penalty ) . toFixed ( 2 ) ) ,
257+ } ;
258+ }
259+
260+ return newAverage ;
238261 }
239-
240- // Use closest if found, otherwise use overall fallback
241- avg = closestAvg || overallFallback ;
242262 }
243263
264+ // If no good window found, use overall average (possibly blended with previous)
265+ if ( previousAverage ) {
266+ return {
267+ points : Number ( ( 0.8 * overallFallback . points + 0.2 * previousAverage . points ) . toFixed ( 2 ) ) ,
268+ tele : Number ( ( 0.8 * overallFallback . tele + 0.2 * previousAverage . tele ) . toFixed ( 2 ) ) ,
269+ penalty : Number ( ( 0.8 * overallFallback . penalty + 0.2 * previousAverage . penalty ) . toFixed ( 2 ) ) ,
270+ } ;
271+ }
272+
273+ return overallFallback ;
274+ } ;
275+
276+ // Create a smoothed average cache for better interpolation
277+ const sortedMatches = matches . sort ( ( a , b ) => new Date ( a . date ) . getTime ( ) - new Date ( b . date ) . getTime ( ) ) ;
278+ const smoothedAverages = new Map < string , { points : number ; tele : number ; penalty : number } > ( ) ;
279+
280+ // Pre-calculate smooth averages for each unique time point with progressive smoothing
281+ const uniqueTimes = [ ...new Set ( sortedMatches . map ( m => m . date ) ) ] ;
282+ let previousAverage : { points : number ; tele : number ; penalty : number } | undefined ;
283+
284+ for ( const time of uniqueTimes ) {
285+ const window = getHourWindow ( time ) ;
286+ const windowKey = `${ window . start } -${ window . end } ` ;
287+
288+ // First try exact hour match
289+ let avg = hourlyAverages . get ( windowKey ) ;
290+
291+ // If no exact match, use smooth averaging with previous context
292+ if ( ! avg ) {
293+ avg = getSmoothAverage ( time , previousAverage ) ;
294+ }
295+
296+ smoothedAverages . set ( time , avg ) ;
297+ previousAverage = avg ; // Store for next iteration
298+ }
299+
300+ // Return matches with smoothed average data
301+ return matches . map ( ( match ) => {
302+ const avg = smoothedAverages . get ( match . date ) || overallFallback ;
303+
244304 // Return the original match data PLUS the average data for comparison
245305 return {
246306 ...match ,
247307 // Keep original values
248308 totalPoints : match . totalPoints ?? 0 ,
249309 tele : match . tele ?? 0 ,
250310 penalty : match . penalty ?? 0 ,
251- // Add average data for graph comparison
311+ // Add smoothed average data for graph comparison
252312 averagePoints : avg . points ,
253313 averageTele : avg . tele ,
254314 averagePenalty : avg . penalty ,
@@ -272,7 +332,7 @@ export function getAverageByMatchType(matches: AllianceInfo[]): MatchTypeAverage
272332 if ( m . matchType === 'QUALIFICATION' ) {
273333 qualTotal += pts ;
274334 qualCount ++ ;
275- } else if ( m . matchType === 'PLAYOFF' ) {
335+ } else if ( m . matchType === 'PLAYOFF' || ( typeof m . matchType === 'string' && ( m . matchType as string ) . includes ( 'FINAL' ) ) ) {
276336 finalsTotal += pts ;
277337 finalsCount ++ ;
278338 }
0 commit comments