@@ -475,3 +475,259 @@ document.querySelectorAll('.image-grid-cell').forEach(cell => {
475475 }
476476 } ) ;
477477} ) ;
478+
479+ // ── Instance Imbalance Interactive Diagram ───────────────────────────────────
480+ ( function ( ) {
481+ const container = document . getElementById ( 'instance-diagram' ) ;
482+ if ( ! container ) return ;
483+
484+ const blobs = [
485+ { x : 14 , y : 40 , r : 34 } ,
486+ { x : 38 , y : 28 , r : 24 } ,
487+ { x : 62 , y : 58 , r : 18 } ,
488+ { x : 28 , y : 72 , r : 13 } ,
489+ { x : 76 , y : 32 , r : 9 } ,
490+ { x : 50 , y : 16 , r : 6 } ,
491+ { x : 82 , y : 68 , r : 5 } ,
492+ { x : 90 , y : 18 , r : 4 } ,
493+ ] ;
494+
495+ const totalArea = blobs . reduce ( ( s , b ) => s + b . r * b . r , 0 ) ;
496+ const N = blobs . length ;
497+ blobs . forEach ( b => {
498+ b . diceW = ( b . r * b . r ) / totalArea ;
499+ b . instW = 1 / N ;
500+ } ) ;
501+
502+ // Pastel VIBGYOR instance colors
503+ const vibgyor = [
504+ '#b8a0d8' , '#8b9bd4' , '#7cbcd4' , '#8cc8a0' ,
505+ '#d4cc80' , '#d8a878' , '#d48888' , '#d4a0b8'
506+ ] ;
507+
508+ let mode = 'dice' ;
509+
510+ // ── Label type tabs ──
511+ const labels = document . createElement ( 'div' ) ;
512+ labels . className = 'diagram-labels' ;
513+
514+ const lblSem = document . createElement ( 'button' ) ;
515+ lblSem . className = 'label-tab active' ;
516+ lblSem . textContent = 'Semantic Labels' ;
517+ lblSem . addEventListener ( 'click' , ( ) => setMode ( 'dice' ) ) ;
518+
519+ const lblInst = document . createElement ( 'button' ) ;
520+ lblInst . className = 'label-tab' ;
521+ lblInst . textContent = 'Instance Labels' ;
522+ lblInst . addEventListener ( 'click' , ( ) => setMode ( 'instance' ) ) ;
523+
524+ labels . appendChild ( lblSem ) ;
525+ labels . appendChild ( lblInst ) ;
526+
527+ // ── Scatter view ──
528+ const scatter = document . createElement ( 'div' ) ;
529+ scatter . className = 'diagram-scatter' ;
530+
531+ blobs . forEach ( ( b , i ) => {
532+ const el = document . createElement ( 'div' ) ;
533+ el . className = 'diagram-blob' ;
534+ el . style . left = b . x + '%' ;
535+ el . style . top = b . y + '%' ;
536+ el . style . width = b . r * 2 + 'px' ;
537+ el . style . height = b . r * 2 + 'px' ;
538+
539+ const ring = document . createElement ( 'div' ) ;
540+ ring . className = 'blob-ring' ;
541+ el . appendChild ( ring ) ;
542+
543+ b . el = el ;
544+ b . ring = ring ;
545+ scatter . appendChild ( el ) ;
546+
547+ el . addEventListener ( 'mouseenter' , ( ) => hi ( i ) ) ;
548+ el . addEventListener ( 'mouseleave' , lo ) ;
549+ } ) ;
550+
551+ // ── Controls row: toggle + scores ──
552+ const controls = document . createElement ( 'div' ) ;
553+ controls . className = 'diagram-controls' ;
554+
555+ const toggle = document . createElement ( 'div' ) ;
556+ toggle . className = 'diagram-toggle' ;
557+
558+ const btnD = document . createElement ( 'button' ) ;
559+ btnD . className = 'diagram-btn active' ;
560+ btnD . textContent = 'Dice Loss' ;
561+ btnD . addEventListener ( 'click' , ( ) => setMode ( 'dice' ) ) ;
562+
563+ const btnI = document . createElement ( 'button' ) ;
564+ btnI . className = 'diagram-btn' ;
565+ btnI . textContent = 'Instance-Aware Dice Loss' ;
566+ btnI . addEventListener ( 'click' , ( ) => setMode ( 'instance' ) ) ;
567+
568+ toggle . appendChild ( btnD ) ;
569+ toggle . appendChild ( btnI ) ;
570+
571+ // ── Score cards ──
572+ const scores = document . createElement ( 'div' ) ;
573+ scores . className = 'diagram-scores' ;
574+
575+ function makeScoreCard ( label ) {
576+ const card = document . createElement ( 'div' ) ;
577+ card . className = 'score-card' ;
578+ const lbl = document . createElement ( 'span' ) ;
579+ lbl . className = 'score-label' ;
580+ lbl . textContent = label ;
581+ const val = document . createElement ( 'span' ) ;
582+ val . className = 'score-value' ;
583+ card . appendChild ( lbl ) ;
584+ card . appendChild ( val ) ;
585+ return { card, val } ;
586+ }
587+
588+ const dscCard = makeScoreCard ( 'DSC' ) ;
589+ const pqCard = makeScoreCard ( 'PQ' ) ;
590+ scores . appendChild ( dscCard . card ) ;
591+ scores . appendChild ( pqCard . card ) ;
592+
593+ controls . appendChild ( toggle ) ;
594+ controls . appendChild ( scores ) ;
595+
596+ const scoreTargets = {
597+ dice : { dsc : 0.95 , pq : 0.38 } ,
598+ instance : { dsc : 0.88 , pq : 0.76 }
599+ } ;
600+ let currentDsc = 0 , currentPq = 0 ;
601+
602+ // ── Vertical contribution bars ──
603+ const barsWrap = document . createElement ( 'div' ) ;
604+ barsWrap . className = 'diagram-bars' ;
605+
606+ blobs . forEach ( ( b , i ) => {
607+ const col = document . createElement ( 'div' ) ;
608+ col . className = 'diagram-bar-col' ;
609+
610+ const pct = document . createElement ( 'span' ) ;
611+ pct . className = 'bar-pct' ;
612+
613+ const track = document . createElement ( 'div' ) ;
614+ track . className = 'bar-track-v' ;
615+
616+ const fill = document . createElement ( 'div' ) ;
617+ fill . className = 'bar-fill-v' ;
618+ track . appendChild ( fill ) ;
619+
620+ const dot = document . createElement ( 'div' ) ;
621+ dot . className = 'bar-dot' ;
622+ const dotSize = Math . max ( 4 , Math . round ( b . r * 0.45 ) ) ;
623+ dot . style . width = dotSize + 'px' ;
624+ dot . style . height = dotSize + 'px' ;
625+
626+ col . appendChild ( pct ) ;
627+ col . appendChild ( track ) ;
628+ col . appendChild ( dot ) ;
629+
630+ b . fill = fill ;
631+ b . pct = pct ;
632+ b . col = col ;
633+ barsWrap . appendChild ( col ) ;
634+
635+ col . addEventListener ( 'mouseenter' , ( ) => hi ( i ) ) ;
636+ col . addEventListener ( 'mouseleave' , lo ) ;
637+ } ) ;
638+
639+ // ── Caption ──
640+ const caption = document . createElement ( 'p' ) ;
641+ caption . className = 'diagram-caption' ;
642+
643+ const top = document . createElement ( 'div' ) ;
644+ top . className = 'diagram-top' ;
645+ top . appendChild ( scatter ) ;
646+ top . appendChild ( barsWrap ) ;
647+
648+ container . appendChild ( labels ) ;
649+ container . appendChild ( top ) ;
650+ container . appendChild ( controls ) ;
651+ container . appendChild ( caption ) ;
652+
653+ // ── Updates ──
654+ function setMode ( m ) {
655+ if ( m === mode ) return ;
656+ mode = m ;
657+ btnD . classList . toggle ( 'active' , mode === 'dice' ) ;
658+ btnI . classList . toggle ( 'active' , mode === 'instance' ) ;
659+ btnI . classList . toggle ( 'rainbow' , mode === 'instance' ) ;
660+ lblSem . classList . toggle ( 'active' , mode === 'dice' ) ;
661+ lblInst . classList . toggle ( 'active' , mode === 'instance' ) ;
662+ lblInst . classList . toggle ( 'rainbow' , mode === 'instance' ) ;
663+ update ( ) ;
664+ }
665+
666+ function animateScore ( el , from , to , flagLow ) {
667+ const duration = 500 ;
668+ const start = performance . now ( ) ;
669+ function tick ( now ) {
670+ const p = Math . min ( 1 , ( now - start ) / duration ) ;
671+ const ease = 1 - Math . pow ( 1 - p , 3 ) ;
672+ const v = from + ( to - from ) * ease ;
673+ el . textContent = v . toFixed ( 2 ) ;
674+ if ( p < 1 ) requestAnimationFrame ( tick ) ;
675+ }
676+ requestAnimationFrame ( tick ) ;
677+ el . classList . toggle ( 'low' , flagLow && to < 0.5 ) ;
678+ el . classList . toggle ( 'high' , flagLow && to >= 0.5 ) ;
679+ }
680+
681+ function update ( ) {
682+ const maxW = Math . max ( ...blobs . map ( b => mode === 'dice' ? b . diceW : b . instW ) ) ;
683+
684+ blobs . forEach ( ( b , i ) => {
685+ const w = mode === 'dice' ? b . diceW : b . instW ;
686+ const n = w / maxW ;
687+
688+ b . el . style . opacity = 0.12 + 0.88 * n ;
689+ b . ring . style . transform = 'scale(' + ( 1 + 0.5 * n ) + ')' ;
690+ b . ring . style . opacity = 0.08 + 0.72 * n ;
691+ b . fill . style . height = ( n * 100 ) + '%' ;
692+ b . pct . textContent = ( w * 100 ) . toFixed ( 1 ) + '%' ;
693+
694+ if ( mode === 'instance' ) {
695+ b . el . style . setProperty ( '--blob-color' , vibgyor [ i ] ) ;
696+ b . col . style . setProperty ( '--blob-color' , vibgyor [ i ] ) ;
697+ } else {
698+ b . el . style . removeProperty ( '--blob-color' ) ;
699+ b . col . style . removeProperty ( '--blob-color' ) ;
700+ }
701+ } ) ;
702+
703+ const t = scoreTargets [ mode ] ;
704+ animateScore ( dscCard . val , currentDsc , t . dsc , false ) ;
705+ animateScore ( pqCard . val , currentPq , t . pq , true ) ;
706+ currentDsc = t . dsc ;
707+ currentPq = t . pq ;
708+
709+ caption . textContent = mode === 'dice'
710+ ? 'With Dice loss, the largest instance owns 48.5% of the gradient. The 3 smallest share just 3.2% \u2014 the model has almost no incentive to detect them.'
711+ : 'Instance-aware: every instance contributes 12.5%, regardless of size. Small lesions receive equal learning signal.' ;
712+ }
713+
714+ // ── Hover cross-highlighting ──
715+ function hi ( idx ) {
716+ container . classList . add ( 'has-highlight' ) ;
717+ blobs . forEach ( ( b , i ) => {
718+ const on = i === idx ;
719+ b . el . classList . toggle ( 'highlighted' , on ) ;
720+ b . col . classList . toggle ( 'highlighted' , on ) ;
721+ } ) ;
722+ }
723+
724+ function lo ( ) {
725+ container . classList . remove ( 'has-highlight' ) ;
726+ blobs . forEach ( b => {
727+ b . el . classList . remove ( 'highlighted' ) ;
728+ b . col . classList . remove ( 'highlighted' ) ;
729+ } ) ;
730+ }
731+
732+ update ( ) ;
733+ } ) ( ) ;
0 commit comments