@@ -127,6 +127,10 @@ function drawBarChartPerBar(cfg){
127127 maxV = isPercentAxis ? 100 : 10 ;
128128 }
129129
130+ // Animation support
131+ if ( ! canvas . _animProgress ) canvas . _animProgress = 0 ;
132+ const animProgress = canvas . _animProgress ;
133+
130134 ctx . clearRect ( 0 , 0 , W , H ) ;
131135
132136 const ticks = cfg . ticks || 5 ;
@@ -152,7 +156,8 @@ function drawBarChartPerBar(cfg){
152156
153157 for ( let i = 0 ; i < n ; i ++ ) {
154158 const v = values [ i ] ;
155- const bh = ( v / maxV ) * plotH ;
159+ const bhFull = ( v / maxV ) * plotH ;
160+ const bh = bhFull * animProgress ; // Apply animation progress
156161 const x = pad . l + i * groupW + ( groupW - barW ) / 2 ;
157162 const y = pad . t + plotH - bh ;
158163
@@ -232,6 +237,10 @@ function drawGroupedBarChart(cfg){
232237 maxV = isPercentAxis ? 100 : 10 ;
233238 }
234239
240+ // Animation support
241+ if ( ! canvas . _animProgress ) canvas . _animProgress = 0 ;
242+ const animProgress = canvas . _animProgress ;
243+
235244 ctx . clearRect ( 0 , 0 , W , H ) ;
236245
237246 const ticks = cfg . ticks || 5 ;
@@ -261,7 +270,8 @@ function drawGroupedBarChart(cfg){
261270 for ( let j = 0 ; j < m ; j ++ ) {
262271 const s = series [ j ] ;
263272 const v = s . values [ i ] ;
264- const bh = ( v / maxV ) * plotH ;
273+ const bhFull = ( v / maxV ) * plotH ;
274+ const bh = bhFull * animProgress ; // Apply animation progress
265275 const x = gx + gap + j * barW ;
266276 const y = pad . t + plotH - bh ;
267277 ctx . fillStyle = s . color ;
@@ -349,37 +359,61 @@ function setupCharts(){
349359 if ( ! data ) continue ;
350360 const labels = data . methods . map ( simplifyMethod ) ;
351361
352- let values , yFmt , title , tipLabel ;
362+ let values , yFmt , tipLabel , htmlTitle ;
353363 if ( metric === "succ" ) {
354364 values = data . succ . slice ( ) ;
355365 yFmt = ( v ) => v . toFixed ( 0 ) + "%" ;
356- title = `${ task } : Success (%)` ;
366+ htmlTitle = `${ task } : Success (%)` ;
357367 tipLabel = "Success:" ;
358368 } else {
359369 values = data . score . slice ( ) ;
360370 yFmt = ( v ) => v . toFixed ( 2 ) ;
361- title = `${ task } : Score` ;
371+ htmlTitle = `${ task } : Score` ;
362372 tipLabel = "Score:" ;
363373 }
364374
375+ // Update HTML title (h4 above canvas)
376+ const canvasId = TASK_CANVAS [ task ] . replace ( '#' , '' ) ;
377+ const canvasEl = document . getElementById ( canvasId ) ;
378+ if ( canvasEl ) {
379+ // Find parent div, then find the h4 sibling before canvas-wrap
380+ const parentDiv = canvasEl . closest ( '.canvas-wrap' ) . parentElement ;
381+ const h4 = parentDiv . querySelector ( 'h4' ) ;
382+ if ( h4 ) {
383+ h4 . textContent = htmlTitle ;
384+ }
385+ }
386+
365387 const outlineIdx = labels . findIndex ( x => x . includes ( "RISE" ) ) ;
366388 const colors = labels . map ( m => m . includes ( "RISE" ) ? "rgba(91,124,250,.88)" : "rgba(120,140,170,.55)" ) ;
367389 const chartMax = ( metric === "succ" ) ? 100 : null ;
368- drawBarChartPerBar ( { canvas : TASK_CANVAS [ task ] , labels, values, colors, outlineIdx, yFmt, title, tipLabel, max : chartMax } ) ;
390+ // Remove 'title' parameter to hide chart internal title
391+ drawBarChartPerBar ( { canvas : TASK_CANVAS [ task ] , labels, values, colors, outlineIdx, yFmt, tipLabel, max : chartMax } ) ;
369392 }
370393 }
371394
395+ // Initialize charts with progress 0 (invisible bars)
396+ for ( const task of TASKS ) {
397+ const canvasEl = q ( TASK_CANVAS [ task ] ) ;
398+ if ( canvasEl ) canvasEl . _animProgress = 0 ;
399+ }
400+ const ablOffline = q ( "#ablationOffline" ) ;
401+ const ablOnline = q ( "#ablationOnline" ) ;
402+ if ( ablOffline ) ablOffline . _animProgress = 0 ;
403+ if ( ablOnline ) ablOnline . _animProgress = 0 ;
404+
372405 metricSel . onchange = renderMainAll ;
373406 renderMainAll ( ) ;
374-
407+
408+ // Store redraw functions
375409 const r1 = drawGroupedBarChart ( {
376410 canvas :"#ablationOffline" ,
377- title : "Ablation: Offline data ratio" ,
411+ // title removed - now in HTML h4
378412 labels : C . offline_ratio . ratio . map ( r => `ratio ${ r } ` ) ,
379413 series :[
380- { name :"Pick&Place Succ.(%) " , values :C . offline_ratio . pick , color :"rgba(91,124,250,.72)" } ,
381- { name :"Sort Acc.(%) " , values :C . offline_ratio . sort , color :"rgba(168,85,247,.55)" } ,
382- { name :"Complete Succ.(%) " , values :C . offline_ratio . comp , color :"rgba(120,140,170,.55)" } ,
414+ { name :"Sort Success Rate " , values :C . offline_ratio . sort , color :"rgba(91,124,250,.72)" } ,
415+ { name :"Pick&Place Success Rate " , values :C . offline_ratio . pick , color :"rgba(168,85,247,.55)" } ,
416+ { name :"Complete Success Rate " , values :C . offline_ratio . comp , color :"rgba(120,140,170,.55)" } ,
383417 ] ,
384418 yFmt :( v ) => v . toFixed ( 0 ) + "%" ,
385419 ticks :5 ,
@@ -388,17 +422,21 @@ function setupCharts(){
388422
389423 const r2 = drawGroupedBarChart ( {
390424 canvas :"#ablationOnline" ,
391- title : "Ablation: Online action/state integration" ,
425+ // title removed - now in HTML h4
392426 labels : C . online_integration . labels ,
393427 series :[
394- { name :"Pick&Place Succ.(%) " , values :C . online_integration . pick , color :"rgba(91,124,250,.72)" } ,
395- { name :"Sort Acc.(%) " , values :C . online_integration . sort , color :"rgba(168,85,247,.55)" } ,
396- { name :"Complete Succ.(%) " , values :C . online_integration . comp , color :"rgba(120,140,170,.55)" } ,
428+ { name :"Sort Success Rate " , values :C . online_integration . sort , color :"rgba(91,124,250,.72)" } ,
429+ { name :"Pick&Place Success Rate " , values :C . online_integration . pick , color :"rgba(168,85,247,.55)" } ,
430+ { name :"Complete Success Rate " , values :C . online_integration . comp , color :"rgba(120,140,170,.55)" } ,
397431 ] ,
398432 yFmt :( v ) => v . toFixed ( 0 ) + "%" ,
399433 ticks :5 ,
400434 boldLabels :[ "(✓,✓)" ]
401435 } ) ;
436+
437+ // Initial draw with 0 progress (invisible bars)
438+ r1 ( ) ;
439+ r2 ( ) ;
402440
403441 let t = null ;
404442 window . addEventListener ( "resize" , ( ) => {
@@ -407,6 +445,56 @@ function setupCharts(){
407445 renderMainAll ( ) ; r1 ( ) ; r2 ( ) ;
408446 } , 120 ) ;
409447 } , { passive :true } ) ;
448+
449+ // Animation on scroll into view
450+ const animateChartOnScroll = ( canvas ) => {
451+ if ( ! canvas || canvas . _animated ) return ;
452+
453+ const observer = new IntersectionObserver ( ( entries ) => {
454+ entries . forEach ( entry => {
455+ if ( entry . isIntersecting && ! canvas . _animated ) {
456+ canvas . _animated = true ;
457+ const duration = 800 ; // Animation duration in ms
458+ const startTime = performance . now ( ) ;
459+
460+ const animate = ( currentTime ) => {
461+ const elapsed = currentTime - startTime ;
462+ const progress = Math . min ( elapsed / duration , 1 ) ;
463+ // Ease-out cubic for smooth deceleration
464+ const eased = 1 - Math . pow ( 1 - progress , 3 ) ;
465+
466+ canvas . _animProgress = eased ;
467+
468+ // Redraw based on which chart type
469+ if ( canvas . id === 'mainChartBrick' || canvas . id === 'mainChartBackpack' || canvas . id === 'mainChartBox' ) {
470+ renderMainAll ( ) ;
471+ } else if ( canvas . id === 'ablationOffline' ) {
472+ r1 ( ) ;
473+ } else if ( canvas . id === 'ablationOnline' ) {
474+ r2 ( ) ;
475+ }
476+
477+ if ( progress < 1 ) {
478+ requestAnimationFrame ( animate ) ;
479+ }
480+ } ;
481+
482+ requestAnimationFrame ( animate ) ;
483+ observer . unobserve ( canvas ) ;
484+ }
485+ } ) ;
486+ } , { threshold : 0.2 } ) ; // Trigger when 20% of chart is visible
487+
488+ observer . observe ( canvas ) ;
489+ } ;
490+
491+ // Setup animation for all charts
492+ for ( const task of TASKS ) {
493+ const canvasEl = q ( TASK_CANVAS [ task ] ) ;
494+ if ( canvasEl ) animateChartOnScroll ( canvasEl ) ;
495+ }
496+ animateChartOnScroll ( q ( "#ablationOffline" ) ) ;
497+ animateChartOnScroll ( q ( "#ablationOnline" ) ) ;
410498}
411499
412500window . addEventListener ( "load" , ( ) => {
0 commit comments