Spaces:
Sleeping
Sleeping
File size: 13,935 Bytes
f5f4567 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | /**
* Side-by-side confusion matrices: baseline vs improved.
* Adapted from research-article-template/app/src/content/embeds/d3-confusion-matrix.html
* Self-contained with inline data.
*/
export const SEED_CONFUSION_MATRIX_HTML = `<div class="d3-confusion-matrix"></div>
<style>
.d3-confusion-matrix { position: relative; }
.d3-confusion-matrix .panels { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 4px; }
.d3-confusion-matrix .panel { flex: 1 1 320px; min-width: 280px; }
.d3-confusion-matrix .panel__title { color: var(--text-color); font-size: 12px; line-height: 1.35; margin: 0 0 6px 0; font-weight: 600; }
.d3-confusion-matrix .axis-label { fill: var(--text-color); font-size: 11px; font-weight: 700; }
.d3-confusion-matrix .cell-text { fill: var(--muted-color); font-size: 11px; pointer-events: none; }
.d3-confusion-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
const bootstrap = () => {
const scriptEl = document.currentScript;
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList && container.classList.contains('d3-confusion-matrix'))) {
const cs = Array.from(document.querySelectorAll('.d3-confusion-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
container = cs[cs.length - 1] || null;
}
if (!container) return;
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
container.style.position = container.style.position || 'relative';
let tip = container.querySelector('.d3-tooltip'); let tipInner;
if (!tip) {
tip = document.createElement('div'); tip.className = 'd3-tooltip';
Object.assign(tip.style, { position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none', padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0', transition: 'opacity .12s ease' });
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign = 'left'; tip.appendChild(tipInner); container.appendChild(tip);
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
const panels = document.createElement('div'); panels.className = 'panels';
const panelA = document.createElement('div'); panelA.className = 'panel';
const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
panelA.appendChild(titleA);
const mountA = document.createElement('div'); panelA.appendChild(mountA);
const panelB = document.createElement('div'); panelB.className = 'panel';
const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved - Baseline, pp)';
panelB.appendChild(titleB);
const mountB = document.createElement('div'); panelB.appendChild(mountB);
panels.appendChild(panelA); panels.appendChild(panelB);
container.appendChild(panels);
const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
const gRootA = svgA.append('g'); const gCellsA = gRootA.append('g'); const gAxesA = gRootA.append('g');
const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
const gRootB = svgB.append('g'); const gCellsB = gRootB.append('g'); const gAxesB = gRootB.append('g');
const classes = ['0','1','2','3','4','5','6','7','8','9'];
const matrixA = [[90,2,1,0,0,0,1,0,5,1],[3,85,5,1,0,1,2,1,1,1],[1,6,70,10,4,4,1,1,1,2],[0,1,8,65,10,10,2,1,1,2],[0,0,2,6,83,3,1,1,3,1],[0,1,2,12,4,70,5,2,2,2],[1,2,1,0,1,2,88,1,3,1],[0,1,1,1,1,1,2,90,1,2],[6,2,2,4,6,3,3,2,70,2],[1,1,1,1,2,1,1,2,1,89]];
const matrixB = [[94,1,0,0,0,0,1,0,3,1],[2,90,3,1,0,0,1,1,1,1],[1,4,78,7,3,3,1,1,1,1],[0,1,5,74,7,8,1,1,1,2],[0,0,1,4,88,2,1,1,2,1],[0,1,1,9,3,78,3,1,2,2],[1,1,1,0,1,1,91,1,2,1],[0,1,1,1,1,1,1,92,1,1],[4,1,1,3,4,2,2,2,79,2],[1,1,1,1,2,1,1,1,1,90]];
const getSequentialColors = (count) => {
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { return window.ColorPalettes.getColors('sequential', count); } } catch (_) {}
const arr = []; for (let i = 0; i < count; i++) arr.push('var(--primary-color)'); return arr;
};
const palette = getSequentialColors(13);
const getDivergingColors = (count) => {
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { return window.ColorPalettes.getColors('diverging', count); } } catch (_) {}
const arr = []; for (let i = 0; i < count; i++) { const t = i / (count - 1); arr.push('color-mix(in srgb, #D64545 ' + Math.round((1-t)*100) + '%, #3A7BD5 ' + Math.round(t*100) + '%)'); } return arr;
};
const margin = { top: 36, right: 24, bottom: 26, left: 56 };
let width = 800, height = 480;
function chooseTextOnBg(bgCss) {
try {
var m = String(bgCss||'').match(/rgba?\\(([^)]+)\\)/);
if (!m) return '#0e1116';
var parts = m[1].split(',').map(function(s){ return parseFloat(s.trim()); });
var srgb = [parts[0],parts[1],parts[2]].map(function(v){ return Math.max(0,Math.min(255,v))/255; });
var linear = srgb.map(function(c){ return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4); });
var L = 0.2126*linear[0]+0.7152*linear[1]+0.0722*linear[2];
return L < 0.5 ? '#ffffff' : '#0e1116';
} catch(_){ return '#0e1116'; }
}
function updateSize() {
width = container.clientWidth || 800;
var gap = 16; var minPanel = 320;
var nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
var panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
var base = Math.max(minPanel, Math.round(panelWidth * 0.92));
height = base;
svgA.attr('viewBox', '0 0 ' + panelWidth + ' ' + height).attr('preserveAspectRatio', 'xMidYMid meet').style('width', '100%').style('height', 'auto');
svgB.attr('viewBox', '0 0 ' + panelWidth + ' ' + height).attr('preserveAspectRatio', 'xMidYMid meet').style('width', '100%').style('height', 'auto');
gRootA.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
gRootB.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var innerWidth = panelWidth - margin.left - margin.right;
var innerHeight = height - margin.top - margin.bottom;
return { innerWidth: innerWidth, innerHeight: innerHeight };
}
function computeValues(matrix) {
var n = classes.length;
var totalsByRow = matrix.map(function(row){ return row.reduce(function(a,b){return a+b;},0); });
var flat = []; var minV = Infinity; var maxV = -Infinity;
for (var r = 0; r < n; r++) { for (var c = 0; c < n; c++) { var count = matrix[r][c]; var value = totalsByRow[r] ? count / totalsByRow[r] : 0; if (value < minV) minV = value; if (value > maxV) maxV = value; flat.push({ r: r, c: c, count: count, value: value }); } }
return { data: flat, minV: minV, maxV: maxV };
}
function getColorScale(values, minV, maxV) {
var hasPalette = palette.length > 0;
if (hasPalette && window.ColorPalettes) { var scale = d3.scaleQuantile().domain(values).range(palette); return function(v){ return scale(v); }; }
var norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
return function(v){ return 'color-mix(in oklab, var(--primary-color) ' + Math.round(norm(v)*100) + '%, var(--surface-bg))'; };
}
function render() {
var sizes = updateSize();
var n = classes.length;
var gridSize = Math.min(sizes.innerWidth, sizes.innerHeight);
var cellSize = gridSize / n;
var x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
var yScale = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
var dataA = computeValues(matrixA);
var colorA = getColorScale(dataA.data.map(function(d){return d.value;}), dataA.minV, dataA.maxV);
gCellsA.selectAll('*').remove();
dataA.data.forEach(function(d) {
gCellsA.append('rect').attr('x', x(d.c)).attr('y', yScale(d.r)).attr('width', x.bandwidth()).attr('height', yScale.bandwidth()).attr('rx', 2).attr('fill', colorA(d.value))
.on('mousemove', function(event) { var p = d3.pointer(event, container); tipInner.innerHTML = '<strong>' + classes[d.r] + '</strong> → <strong>' + classes[d.c] + '</strong><br/>' + (d.value*100).toFixed(1) + '% (' + d.count + ')'; tip.style.transform = 'translate(' + (p[0]+10) + 'px, ' + (p[1]+10) + 'px)'; tip.style.opacity = '1'; })
.on('mouseleave', function() { tip.style.opacity = '0'; });
gCellsA.append('text').attr('class', 'cell-text').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle').attr('x', x(d.c) + x.bandwidth()/2).attr('y', yScale(d.r) + yScale.bandwidth()/2).text(Math.round(d.value*100));
});
gAxesA.selectAll('*').remove();
classes.forEach(function(c, i) {
gAxesA.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('x', x(i) + x.bandwidth()/2).attr('y', -8).text(c);
gAxesA.append('text').attr('class', 'axis-label').attr('text-anchor', 'end').attr('x', -8).attr('y', yScale(i) + yScale.bandwidth()/2).attr('dominant-baseline', 'middle').text(c);
});
gAxesA.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('x', gridSize/2).attr('y', sizes.innerHeight + 20).text('Predicted');
gAxesA.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('transform', 'translate(' + (-40) + ',' + (gridSize/2) + ') rotate(-90)').text('Actual');
var dataB = computeValues(matrixB);
var diverging = getDivergingColors(13);
var mapA = new Map(dataA.data.map(function(d){ return [d.r + '-' + d.c, d.value]; }));
var delta = dataB.data.map(function(d){ return { r: d.r, c: d.c, count: d.count, value: d.value - (mapA.get(d.r + '-' + d.c) || 0) }; });
var maxAbsDelta = Math.max(0.01, d3.max(delta, function(d){ return Math.abs(d.value); }) || 0.01);
var colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
gCellsB.selectAll('*').remove();
delta.forEach(function(d, i) {
gCellsB.append('rect').attr('x', x(d.c)).attr('y', yScale(d.r)).attr('width', x.bandwidth()).attr('height', yScale.bandwidth()).attr('rx', 2).attr('fill', colorB(d.value))
.on('mousemove', function(event) { var p = d3.pointer(event, container); var a = dataA.data.find(function(x){ return x.r===d.r && x.c===d.c; }); var b = dataB.data.find(function(x){ return x.r===d.r && x.c===d.c; }); var dv = ((b?b.value:0)-(a?a.value:0))*100; tipInner.innerHTML = '<strong>' + classes[d.r] + '</strong> → <strong>' + classes[d.c] + '</strong><br/>baseline ' + (a?a.value*100:0).toFixed(1) + '%<br/>improved ' + (b?b.value*100:0).toFixed(1) + '%<br/>delta ' + dv.toFixed(1) + ' pp'; tip.style.transform = 'translate(' + (p[0]+10) + 'px, ' + (p[1]+10) + 'px)'; tip.style.opacity = '1'; })
.on('mouseleave', function() { tip.style.opacity = '0'; });
gCellsB.append('text').attr('class', 'cell-text').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle').attr('x', x(d.c) + x.bandwidth()/2).attr('y', yScale(d.r) + yScale.bandwidth()/2).text(Math.round(d.value*100));
});
gAxesB.selectAll('*').remove();
classes.forEach(function(c, i) {
gAxesB.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('x', x(i) + x.bandwidth()/2).attr('y', -8).text(c);
gAxesB.append('text').attr('class', 'axis-label').attr('text-anchor', 'end').attr('x', -8).attr('y', yScale(i) + yScale.bandwidth()/2).attr('dominant-baseline', 'middle').text(c);
});
gAxesB.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('x', gridSize/2).attr('y', sizes.innerHeight + 20).text('Predicted');
gAxesB.append('text').attr('class', 'axis-label').attr('text-anchor', 'middle').attr('transform', 'translate(' + (-40) + ',' + (gridSize/2) + ') rotate(-90)').text('Actual');
}
render();
if (window.ResizeObserver) { new ResizeObserver(function(){ render(); }).observe(container); }
};
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function(){ ensureD3(bootstrap); }, { once: true }); } else { ensureD3(bootstrap); }
})();
<\/script>`;
|