Skip to content

Commit a02f6c7

Browse files
aymuos15claude
andcommitted
feat: interactive instance imbalance diagram on research page
Scatter view with blobs + vertical contribution bars, toggle between Dice Loss (semantic) and Instance-Aware Dice Loss (instance labels) with animated DSC/PQ scores and pastel VIBGYOR rainbow colors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bfef107 commit a02f6c7

4 files changed

Lines changed: 541 additions & 1 deletion

File tree

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ <h1 class="name-link">Soumya Snigdha Kundu</h1>
8282
</section>
8383

8484
<section id="research" class="section">
85+
<div class="instance-diagram" id="instance-diagram"></div>
8586
<p>My research focuses on handling instance imbalance in brain tumour segmentation.</p>
8687
<p>But I enjoy coding in general. Maybe an ode to my undergrad in CS. It's why I worked at Cosine to work on Large Code Models and why I love building developer tools.</p>
8788
</section>

script.js

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)