@@ -91,6 +91,33 @@ let currentPeriod: ChartPeriod = 'day';
9191let pendingView : typeof currentView | null = null ;
9292let pendingPeriod : ChartPeriod | null = null ;
9393
94+ type DisplayMode = 'actual' | 'rolling' ;
95+ let currentDisplayMode : DisplayMode = 'actual' ;
96+ const ROLLING_WINDOW : Record < ChartPeriod , number > = { day : 7 , week : 4 , month : 3 } ;
97+
98+ function computeRollingAverage ( data : number [ ] , window : number ) : number [ ] {
99+ return data . map ( ( _ , i ) => {
100+ const start = Math . max ( 0 , i - window + 1 ) ;
101+ const slice = data . slice ( start , i + 1 ) ;
102+ return Math . round ( slice . reduce ( ( a , b ) => a + b , 0 ) / slice . length ) ;
103+ } ) ;
104+ }
105+
106+ function getRollingLabel ( ) : string {
107+ const w = ROLLING_WINDOW [ currentPeriod ] ;
108+ const unit = currentPeriod === 'day' ? 'day' : currentPeriod === 'week' ? 'week' : 'month' ;
109+ return `${ w } -${ unit } rolling avg` ;
110+ }
111+
112+ function getChartTitle ( ) : string {
113+ const periodMeta = PERIOD_LABELS [ currentPeriod ] ;
114+ let titleText = currentView === 'cost' ? periodMeta . costTitle : periodMeta . title ;
115+ if ( currentDisplayMode === 'rolling' && ( currentView === 'total' || currentView === 'cost' ) ) {
116+ titleText += ` (${ getRollingLabel ( ) } )` ;
117+ }
118+ return titleText ;
119+ }
120+
94121/** Returns period data for the current period, falling back to legacy flat fields. */
95122function getActivePeriodData ( data : InitialChartData ) : ChartPeriodData {
96123 if ( data . periods ) {
@@ -138,7 +165,7 @@ function renderLayout(data: InitialChartData): void {
138165 const header = el ( 'div' , 'header' ) ;
139166 const headerLeft = el ( 'div' , 'header-left' ) ;
140167 const icon = el ( 'span' , 'header-icon' , '📈' ) ;
141- const title = el ( 'span' , 'header-title' , currentView === 'cost' ? PERIOD_LABELS [ currentPeriod ] . costTitle : PERIOD_LABELS [ currentPeriod ] . title ) ;
168+ const title = el ( 'span' , 'header-title' , getChartTitle ( ) ) ;
142169 title . id = 'chart-title' ;
143170 headerLeft . append ( icon , title ) ;
144171 const buttons = el ( 'div' , 'button-row' ) ;
@@ -215,7 +242,10 @@ function renderLayout(data: InitialChartData): void {
215242 repoBtn . id = 'view-repository' ;
216243 const costBtn = el ( 'button' , `toggle${ currentView === 'cost' ? ' active' : '' } ` , '💰 Est. Cost' ) ;
217244 costBtn . id = 'view-cost' ;
218- toggles . append ( totalBtn , modelBtn , editorBtn , repoBtn , costBtn ) ;
245+ const rollingApplicableNow = currentView === 'total' || currentView === 'cost' ;
246+ const rollingBtn = el ( 'button' , `toggle rolling-toggle${ currentDisplayMode === 'rolling' ? ' active' : '' } ${ rollingApplicableNow ? '' : ' hidden' } ` , '📈 Rolling Avg' ) ;
247+ rollingBtn . id = 'view-rolling' ;
248+ toggles . append ( totalBtn , modelBtn , editorBtn , repoBtn , costBtn , rollingBtn ) ;
219249
220250 const canvasWrap = el ( 'div' , 'canvas-wrap' ) ;
221251 const canvas = document . createElement ( 'canvas' ) ;
@@ -293,7 +323,7 @@ function updateSummaryCards(data: InitialChartData): void {
293323 updateCard ( 'card-total-sessions' , null , periodData . totalSessions . toLocaleString ( ) ) ;
294324
295325 const title = document . getElementById ( 'chart-title' ) ;
296- if ( title ) { title . textContent = currentView === 'cost' ? periodMeta . costTitle : periodMeta . title ; }
326+ if ( title ) { title . textContent = getChartTitle ( ) ; }
297327
298328 const footer = document . getElementById ( 'chart-footer' ) ;
299329 if ( footer ) {
@@ -348,6 +378,9 @@ function wireInteractions(data: InitialChartData): void {
348378 const btn = document . getElementById ( id ) ;
349379 btn ?. addEventListener ( 'click' , ( ) => { void switchView ( view , data ) ; } ) ;
350380 } ) ;
381+
382+ const rollingToggle = document . getElementById ( 'view-rolling' ) ;
383+ rollingToggle ?. addEventListener ( 'click' , ( ) => { void switchDisplayMode ( data ) ; } ) ;
351384}
352385
353386async function setupChart ( canvas : HTMLCanvasElement , data : InitialChartData ) : Promise < void > {
@@ -405,8 +438,17 @@ async function switchView(view: 'total' | 'model' | 'editor' | 'repository' | 'c
405438 if ( currentView === view ) {
406439 return ;
407440 }
441+ const rollingApplicable = view === 'total' || view === 'cost' ;
442+ if ( ! rollingApplicable ) {
443+ currentDisplayMode = 'actual' ;
444+ }
408445 currentView = view ;
409446 setActiveView ( view ) ;
447+ const rollingBtnEl = document . getElementById ( 'view-rolling' ) ;
448+ if ( rollingBtnEl ) {
449+ rollingBtnEl . classList . toggle ( 'hidden' , ! rollingApplicable ) ;
450+ rollingBtnEl . classList . toggle ( 'active' , rollingApplicable && currentDisplayMode === 'rolling' ) ;
451+ }
410452 updateSummaryCards ( data ) ;
411453 if ( ! chart ) {
412454 return ;
@@ -445,6 +487,27 @@ function setActiveView(view: 'total' | 'model' | 'editor' | 'repository' | 'cost
445487 } ) ;
446488}
447489
490+ function setActiveDisplayMode ( mode : DisplayMode ) : void {
491+ const btn = document . getElementById ( 'view-rolling' ) ;
492+ if ( ! btn ) { return ; }
493+ btn . classList . toggle ( 'active' , mode === 'rolling' ) ;
494+ }
495+
496+ async function switchDisplayMode ( data : InitialChartData ) : Promise < void > {
497+ currentDisplayMode = currentDisplayMode === 'actual' ? 'rolling' : 'actual' ;
498+ setActiveDisplayMode ( currentDisplayMode ) ;
499+ updateSummaryCards ( data ) ;
500+ if ( ! chart ) { return ; }
501+ const canvas = chart . canvas as HTMLCanvasElement | null ;
502+ chart . destroy ( ) ;
503+ if ( ! canvas ) { return ; }
504+ const ctx = canvas . getContext ( '2d' ) ;
505+ if ( ! ctx ) { return ; }
506+ await loadChartModule ( ) ;
507+ if ( ! Chart ) { return ; }
508+ chart = new Chart ( ctx , createConfig ( currentView , data ) ) ;
509+ }
510+
448511function createConfig ( view : 'total' | 'model' | 'editor' | 'repository' | 'cost' , data : InitialChartData ) : ChartConfig {
449512 const period = getActivePeriodData ( data ) ;
450513
@@ -480,17 +543,23 @@ function createConfig(view: 'total' | 'model' | 'editor' | 'repository' | 'cost'
480543 } ;
481544
482545 if ( view === 'total' ) {
546+ const isRolling = currentDisplayMode === 'rolling' ;
547+ const rollingLabel = getRollingLabel ( ) ;
548+ const tokenData = isRolling ? computeRollingAverage ( period . tokensData , ROLLING_WINDOW [ currentPeriod ] ) : period . tokensData ;
483549 return {
484550 type : 'bar' as const ,
485551 data : {
486552 labels : period . labels ,
487553 datasets : [
488554 {
489- label : 'Tokens' ,
490- data : period . tokensData ,
491- backgroundColor : 'rgba(54, 162, 235, 0.6)' ,
555+ label : isRolling ? rollingLabel : 'Tokens' ,
556+ data : tokenData ,
557+ backgroundColor : isRolling ? 'rgba(54, 162, 235, 0.15)' : 'rgba(54, 162, 235, 0.6)' ,
492558 borderColor : 'rgba(54, 162, 235, 1)' ,
493- borderWidth : 1 ,
559+ borderWidth : isRolling ? 2 : 1 ,
560+ type : isRolling ? 'line' as const : undefined ,
561+ tension : isRolling ? 0.4 : undefined ,
562+ fill : isRolling ? false : undefined ,
494563 yAxisID : 'y'
495564 } ,
496565 {
@@ -532,17 +601,23 @@ function createConfig(view: 'total' | 'model' | 'editor' | 'repository' | 'cost'
532601 const datasets = view === 'model' ? period . modelDatasets : view === 'repository' ? period . repositoryDatasets : period . editorDatasets ;
533602
534603 if ( view === 'cost' ) {
604+ const isRolling = currentDisplayMode === 'rolling' ;
605+ const rollingLabel = getRollingLabel ( ) ;
606+ const costData = isRolling ? computeRollingAverage ( period . costData , ROLLING_WINDOW [ currentPeriod ] ) : period . costData ;
535607 return {
536608 type : 'bar' as const ,
537609 data : {
538610 labels : period . labels ,
539611 datasets : [
540612 {
541- label : 'Est. Cost (TBB)' ,
542- data : period . costData ,
543- backgroundColor : 'rgba(34, 197, 94, 0.6)' ,
613+ label : isRolling ? ` ${ rollingLabel } (TBB)` : 'Est. Cost (TBB)' ,
614+ data : costData ,
615+ backgroundColor : isRolling ? 'rgba(34, 197, 94, 0.15)' : 'rgba(34, 197, 94, 0.6)' ,
544616 borderColor : 'rgba(34, 197, 94, 1)' ,
545- borderWidth : 1 ,
617+ borderWidth : isRolling ? 2 : 1 ,
618+ type : isRolling ? 'line' as const : undefined ,
619+ tension : isRolling ? 0.4 : undefined ,
620+ fill : isRolling ? false : undefined ,
546621 yAxisID : 'y'
547622 }
548623 ]
0 commit comments