@@ -170,6 +170,7 @@ <h2>Models</h2>
170170let currentRange = '7d' ;
171171let barMode = 'co2' ;
172172let lastData = null ;
173+ let lastEmissionsTotalGrams = 0 ;
173174
174175function chartColors ( ) {
175176 const light = document . body . classList . contains ( 'light' ) ;
@@ -490,7 +491,19 @@ <h2>Models</h2>
490491 } ) . join ( '' ) ;
491492}
492493
493- // CO₂/token over time — only points where tokens were being generated.
494+ // The backend always emits step as one of "5m0s", "1h0m0s", "6h0m0s"
495+ // (Go's time.Duration.String()) -- parse the h/m/s components present.
496+ function parseStepSeconds ( stepStr ) {
497+ const h = / ( \d + ) h / . exec ( stepStr ) ;
498+ const m = / ( \d + ) m / . exec ( stepStr ) ;
499+ const s = / ( \d + ) s / . exec ( stepStr ) ;
500+ return ( h ?+ h [ 1 ] * 3600 :0 ) + ( m ?+ m [ 1 ] * 60 :0 ) + ( s ?+ s [ 1 ] :0 ) ;
501+ }
502+
503+ // Cumulative CO₂ emissions over the selected window — integrates the
504+ // co2_grams_per_hour rate series using the rectangle rule (rate × step
505+ // duration), running-summed. Always populated as long as the range has any
506+ // points, even during idle periods (the total simply grows slowly).
494507function renderTimeSeries ( data ) {
495508 const pts = data . points || [ ] ;
496509 if ( ! pts . length ) return ;
@@ -504,59 +517,49 @@ <h2>Models</h2>
504517 return d . toLocaleDateString ( [ ] , { month :'short' , day :'numeric' } ) ;
505518 } ;
506519
507- // Use CO₂/token where available; show null (gap) during idle periods
508- const tokenPts = pts . filter ( p => p . co2_mg_per_token > 0 ) ;
509- const hasTokens = tokenPts . length > 0 ;
510-
511- const datasets = hasTokens ? [
512- {
513- label : 'Cluster avg CO₂/token (mg)' ,
514- data : pts . map ( p => p . co2_mg_per_token > 0 ? + p . co2_mg_per_token . toFixed ( 3 ) : null ) ,
515- borderColor : '#22c55e' ,
516- backgroundColor : 'rgba(34,197,94,0.08)' ,
517- fill : true , tension : 0.3 , pointRadius : 0 , spanGaps : false ,
518- } ,
519- ] : [
520- // Fallback: show CO₂/hr when no token data in range
521- {
522- label : 'CO₂ rate (kg/hr)' ,
523- data : pts . map ( p => + ( p . co2_grams_per_hour / 1000 ) . toFixed ( 4 ) ) ,
524- borderColor : '#6366f1' , backgroundColor : 'rgba(99,102,241,0.1)' ,
520+ const stepHours = parseStepSeconds ( data . step ) / 3600 ;
521+ let running = 0 ;
522+ const cumulativeGrams = pts . map ( p => {
523+ running += p . co2_grams_per_hour * stepHours ;
524+ return + running . toFixed ( 2 ) ;
525+ } ) ;
526+ lastEmissionsTotalGrams = running ;
527+
528+ const totalLabel = running >= 1000
529+ ? ( running / 1000 ) . toFixed ( 2 ) + ' kg CO₂e'
530+ : running . toFixed ( 1 ) + ' g CO₂e' ;
531+
532+ const chartData = {
533+ labels : pts . map ( mkLabel ) ,
534+ datasets : [ {
535+ label : 'Cumulative CO₂ (g)' ,
536+ data : cumulativeGrams ,
537+ borderColor : '#6366f1' ,
538+ backgroundColor : 'rgba(99,102,241,0.1)' ,
525539 fill : true , tension : 0.3 , pointRadius : 0 ,
526- }
527- ] ;
528-
529- const avgToken = hasTokens
530- ? ( tokenPts . reduce ( ( s , p ) => s + p . co2_mg_per_token , 0 ) / tokenPts . length ) . toFixed ( 2 ) + ' mg CO₂/token avg'
531- : 'no active generation in range — showing CO₂/hr' ;
532-
533- const yTitle = hasTokens ? 'mg CO₂ / token' : 'kg CO₂ / hr' ;
534- const tooltipFmt = hasTokens
535- ? c => ` ${ c . dataset . label } : ${ c . parsed . y != null ? c . parsed . y . toFixed ( 3 ) : '—' } mg/token`
536- : c => ` ${ c . parsed . y . toFixed ( 3 ) } kg CO₂/hr` ;
537-
538- const chartData = { labels : pts . map ( mkLabel ) , datasets } ;
540+ } ] ,
541+ } ;
539542 const lc = chartColors ( ) ;
540543 const options = {
541544 responsive : true ,
542545 interaction : { mode :'index' , intersect :false } ,
543546 plugins : {
544547 legend : { labels :{ color :lc . legend , font :{ size :10 } , boxWidth :12 } } ,
545- title : { display :true , text :`${ rangeLabel } — ${ avgToken } ` ,
548+ title : { display :true , text :`${ rangeLabel } — Total: ${ totalLabel } ` ,
546549 color :lc . tick , font :{ size :11 , weight :'normal' } , padding :{ bottom :8 } } ,
547- tooltip : { callbacks : { label : tooltipFmt } }
550+ tooltip : { callbacks : { label : c => ` ${ c . parsed . y . toFixed ( 2 ) } g CO₂ cumulative` } }
548551 } ,
549552 scales : {
550553 x : { ticks :{ color :lc . tick , font :{ size :10 } , maxTicksLimit :10 } , grid :{ color :lc . grid } } ,
551554 y : { ticks :{ color :lc . tick , font :{ size :10 } } , grid :{ color :lc . grid } ,
552- title :{ display :true , text :yTitle , color :lc . tick , font :{ size :10 } } ,
555+ title :{ display :true , text :'g CO₂ (cumulative)' , color :lc . tick , font :{ size :10 } } ,
553556 beginAtZero :true }
554557 }
555558 } ;
556559
557560 if ( lineChart ) {
558561 lineChart . data = chartData ;
559- lineChart . options . plugins . title . text = `${ rangeLabel } — ${ avgToken } ` ;
562+ lineChart . options . plugins . title . text = `${ rangeLabel } — Total: ${ totalLabel } ` ;
560563 lineChart . update ( 'none' ) ;
561564 } else {
562565 lineChart = new Chart ( document . getElementById ( 'lineChart' ) , { type :'line' , data :chartData , options } ) ;
0 commit comments