spreadsheetlabeller / static /feature_eda.html
zofiasmolenasana
Merge value+attribute labels, add header label, RAG eval improvements
d051588 unverified
Raw
History Blame Contribute Delete
18.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature EDA – MLOps</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
:root {
--bg-0: #0b1120; --bg-1: #0f172a; --bg-2: #1e293b; --bg-3: #334155;
--border: #334155; --border-hover: #475569;
--text-0: #f1f5f9; --text-1: #e2e8f0; --text-2: #cbd5e1; --text-3: #94a3b8; --text-4: #64748b;
--blue: #3b82f6; --blue-dim: #1e3a5f;
--green: #34d399; --green-dim: #14532d;
--red: #f87171; --red-dim: #450a0a;
--amber: #f59e0b; --purple: #a855f7; --cyan: #06b6d4;
--pink: #ec4899; --orange: #f97316;
--sidebar-w: 220px; --topbar-h: 52px;
--radius: 10px; --radius-sm: 6px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;background:var(--bg-0);color:var(--text-1);min-height:100vh}
.topbar{position:fixed;top:0;left:0;right:0;z-index:200;height:var(--topbar-h);background:var(--bg-2);
border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 20px;gap:14px}
.topbar .logo{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700;color:var(--text-0);letter-spacing:-.3px}
.topbar .logo svg{width:22px;height:22px;color:var(--blue)}
.topbar .spacer{flex:1}
.topbar .back{color:var(--text-3);font-size:13px;text-decoration:none;padding:6px 12px;border-radius:var(--radius-sm);transition:all .15s}
.topbar .back:hover{color:var(--text-1);background:var(--bg-3)}
.main{margin-top:var(--topbar-h);padding:28px 32px;max-width:1400px;margin-left:auto;margin-right:auto}
.page-title{font-size:22px;font-weight:700;color:var(--text-0);margin-bottom:4px}
.page-subtitle{font-size:13px;color:var(--text-3);margin-bottom:24px}
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:24px}
.stat-card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px}
.stat-card .label{font-size:11px;font-weight:600;color:var(--text-4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.stat-card .value{font-size:22px;font-weight:700;color:var(--text-0)}
.stat-card .sub{font-size:11px;color:var(--text-3);margin-top:2px}
.card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;margin-bottom:20px}
.card-title{font-size:15px;font-weight:700;color:var(--text-0);margin-bottom:16px;display:flex;align-items:center;gap:8px}
.card-title svg{width:16px;height:16px;color:var(--text-3)}
.tabs{display:flex;gap:2px;margin-bottom:16px;background:var(--bg-1);border-radius:var(--radius-sm);padding:3px;overflow-x:auto}
.tab{padding:7px 14px;border-radius:4px;font-size:12px;font-weight:600;color:var(--text-3);cursor:pointer;
transition:all .15s;white-space:nowrap;border:none;background:none}
.tab:hover{color:var(--text-1)}
.tab.active{background:var(--bg-3);color:var(--text-0)}
.chart-container{position:relative;height:340px;margin-bottom:8px}
.chart-container canvas{width:100%!important;height:100%!important}
.heatmap-grid{overflow-x:auto}
table.heatmap{border-collapse:collapse;font-size:12px;width:100%}
table.heatmap th{padding:6px 10px;font-weight:600;color:var(--text-0);background:var(--bg-1);
position:sticky;top:0;text-align:left;white-space:nowrap;border:1px solid var(--bg-3)}
table.heatmap td{padding:5px 10px;border:1px solid var(--bg-3);text-align:center;font-variant-numeric:tabular-nums}
table.heatmap .feature-name{text-align:left;font-weight:500;color:var(--text-2);white-space:nowrap;
position:sticky;left:0;background:var(--bg-2);z-index:1}
.top-features-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
.top-feature-card{background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px 16px}
.top-feature-card .class-name{font-size:13px;font-weight:700;color:var(--text-0);margin-bottom:2px;
display:flex;align-items:center;gap:8px}
.top-feature-card .class-count{font-size:11px;color:var(--text-4);margin-bottom:10px}
.feat-row{display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.feat-row:last-child{border-bottom:none}
.feat-rank{font-size:10px;font-weight:700;color:var(--text-4);width:18px;text-align:right}
.feat-name{font-size:12px;color:var(--text-2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.feat-bar-wrap{width:100px;height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden}
.feat-bar{height:100%;border-radius:3px}
.feat-val{font-size:11px;font-weight:600;color:var(--text-1);width:44px;text-align:right;font-variant-numeric:tabular-nums}
.filter-row{display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
.filter-row label{font-size:12px;color:var(--text-3);font-weight:600}
.filter-row select{padding:5px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);
background:var(--bg-1);color:var(--text-1);font-size:12px}
.loading{text-align:center;padding:60px;color:var(--text-3);font-size:14px}
.loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--bg-3);
border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}
@keyframes spin{to{transform:rotate(360deg)}}
.group-label{font-size:10px;font-weight:700;color:var(--text-4);text-transform:uppercase;letter-spacing:1px;
padding:8px 0 4px;border-top:1px solid var(--bg-3);margin-top:4px}
.legend-row{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px}
.legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-3)}
.legend-dot{width:10px;height:10px;border-radius:2px}
</style>
</head>
<body>
<div class="topbar">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/></svg>
Excel Chunker
</div>
<div class="spacer"></div>
<a class="back" href="/train">&larr; Training</a>
<a class="back" href="/">Labeler</a>
</div>
<div class="main">
<h1 class="page-title">Feature EDA</h1>
<p class="page-subtitle">Exploratory analysis of handcrafted features by cell class — understand which features discriminate each class</p>
<div id="loading" class="loading">
<div class="spinner"></div><br>
Computing feature statistics across all labeled data…
</div>
<div id="content" style="display:none">
<!-- Summary stats -->
<div class="stats-row" id="stats-row"></div>
<!-- Top discriminative features per class -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg>
Top Discriminative Features per Class
</div>
<p style="font-size:12px;color:var(--text-3);margin-bottom:14px">
Top 10 features whose class mean differs most from the global mean — higher abs diff = more distinctive for this class
</p>
<div class="top-features-grid" id="top-features"></div>
</div>
<!-- Radar / bar chart comparison -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
Feature Comparison by Class
</div>
<div class="filter-row">
<label>Feature group:</label>
<select id="group-select">
<option value="all">All features</option>
<option value="position">Position (0-2)</option>
<option value="content">Content stats (3-11)</option>
<option value="formatting">Formatting (12-27)</option>
<option value="formula">Formula (28-31)</option>
<option value="content_type">Content type (32-43)</option>
<option value="keywords">Keywords (44-54)</option>
<option value="textual">Textual/Punctuation (56-70)</option>
<option value="numerical">TUTA numerical (71-74)</option>
<option value="col_row">Column/Row context (75-82)</option>
<option value="cohesion">TableSense cohesion (83-88)</option>
</select>
<label>Mode:</label>
<select id="mode-select">
<option value="mean">Mean activation</option>
<option value="prevalence">Prevalence (% &gt; 0)</option>
<option value="diff">Diff from global mean</option>
</select>
</div>
<div class="chart-container"><canvas id="comparison-chart"></canvas></div>
</div>
<!-- Class distribution bar -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
Class Distribution
</div>
<div class="chart-container" style="height:260px"><canvas id="class-chart"></canvas></div>
</div>
<!-- Full heatmap table -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg>
Full Feature Heatmap (Mean Activation)
</div>
<p style="font-size:12px;color:var(--text-3);margin-bottom:12px">
Color intensity = mean value for that class. Hover for exact values.
</p>
<div class="heatmap-grid" id="heatmap-container"></div>
</div>
</div>
</div>
<script>
const CLASS_COLORS = {
value: '#3b82f6',
attribute: '#f59e0b',
col_header: '#34d399',
row_header: '#06b6d4',
aggregation: '#a855f7',
metadata: '#ec4899',
comment: '#f97316',
empty: '#64748b',
junk: '#f87171',
};
const FEATURE_GROUPS = {
position: [0, 2],
content: [3, 11],
formatting: [12, 27],
formula: [28, 31],
content_type: [32, 43],
keywords: [44, 54],
merge_span: [55, 55],
textual: [56, 70],
numerical: [71, 74],
col_row: [75, 82],
cohesion: [83, 88],
};
let DATA = null;
let comparisonChart = null;
async function loadData() {
const resp = await fetch('/api/train/feature_eda');
DATA = await resp.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
renderAll();
}
function renderAll() {
renderStats();
renderTopFeatures();
renderComparisonChart();
renderClassChart();
renderHeatmap();
}
function renderStats() {
const el = document.getElementById('stats-row');
const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0);
el.innerHTML = `
<div class="stat-card"><div class="label">Total Cells</div><div class="value">${DATA.total_cells.toLocaleString()}</div></div>
<div class="stat-card"><div class="label">Features</div><div class="value">${DATA.num_features}</div></div>
<div class="stat-card"><div class="label">Classes</div><div class="value">${classes.length}</div></div>
<div class="stat-card"><div class="label">Largest Class</div><div class="value">${(() => {
let max = 0, name = '';
for (const [k,v] of Object.entries(DATA.class_counts)) { if (v > max) { max = v; name = k; } }
return name;
})()}</div><div class="sub">${Math.max(...Object.values(DATA.class_counts)).toLocaleString()} cells</div></div>
<div class="stat-card"><div class="label">Smallest Class</div><div class="value">${(() => {
let min = Infinity, name = '';
for (const [k,v] of Object.entries(DATA.class_counts)) { if (v > 0 && v < min) { min = v; name = k; } }
return name;
})()}</div><div class="sub">${(() => {
let min = Infinity; for (const v of Object.values(DATA.class_counts)) { if (v > 0 && v < min) min = v; } return min.toLocaleString();
})()} cells</div></div>
`;
}
function renderTopFeatures() {
const el = document.getElementById('top-features');
const classes = Object.keys(DATA.top_features_per_class);
el.innerHTML = classes.map(cls => {
const feats = DATA.top_features_per_class[cls];
const count = DATA.class_counts[cls];
const color = CLASS_COLORS[cls] || '#94a3b8';
const maxDiff = Math.max(...feats.map(f => f.abs_diff), 0.01);
return `
<div class="top-feature-card">
<div class="class-name"><span class="legend-dot" style="background:${color}"></span>${cls}</div>
<div class="class-count">${count.toLocaleString()} cells</div>
${feats.map((f, i) => `
<div class="feat-row">
<span class="feat-rank">${i+1}</span>
<span class="feat-name" title="${f.feature}">${f.feature}</span>
<div class="feat-bar-wrap">
<div class="feat-bar" style="width:${(f.abs_diff / maxDiff * 100).toFixed(1)}%;background:${f.class_mean > f.global_mean ? color : 'var(--red)'}"></div>
</div>
<span class="feat-val" title="class: ${f.class_mean}, global: ${f.global_mean}">${f.class_mean > f.global_mean ? '+' : ''}${f.abs_diff.toFixed(3)}</span>
</div>
`).join('')}
</div>
`;
}).join('');
}
function getFilteredFeatures() {
const group = document.getElementById('group-select').value;
if (group === 'all') return DATA.features;
const [lo, hi] = FEATURE_GROUPS[group] || [0, DATA.num_features - 1];
return DATA.features.filter(f => f.index >= lo && f.index <= hi);
}
function renderComparisonChart() {
const mode = document.getElementById('mode-select').value;
const feats = getFilteredFeatures();
const labels = feats.map(f => f.name);
const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0);
const datasets = classes.map(cls => {
const color = CLASS_COLORS[cls] || '#94a3b8';
return {
label: cls,
data: feats.map(f => {
const ci = f.classes[cls];
if (!ci) return 0;
if (mode === 'mean') return ci.mean;
if (mode === 'prevalence') return ci.prevalence;
return ci.diff_from_global;
}),
backgroundColor: color + '99',
borderColor: color,
borderWidth: 1,
};
});
const ctx = document.getElementById('comparison-chart').getContext('2d');
if (comparisonChart) comparisonChart.destroy();
comparisonChart = new Chart(ctx, {
type: 'bar',
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 }, boxWidth: 12 } },
tooltip: {
callbacks: {
label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(4)}`,
}
},
},
scales: {
x: {
ticks: { color: '#64748b', font: { size: 10 }, maxRotation: 55, minRotation: 30 },
grid: { color: 'rgba(255,255,255,.04)' },
},
y: {
ticks: { color: '#64748b', font: { size: 10 } },
grid: { color: 'rgba(255,255,255,.06)' },
title: { display: true, text: mode === 'prevalence' ? 'Fraction of cells with feature > 0' : mode === 'diff' ? 'Difference from global mean' : 'Mean value', color: '#94a3b8', font: { size: 11 } },
},
},
interaction: { mode: 'index', intersect: false },
},
});
}
function renderClassChart() {
const ctx = document.getElementById('class-chart').getContext('2d');
const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0);
const counts = classes.map(k => DATA.class_counts[k]);
const colors = classes.map(k => CLASS_COLORS[k] || '#94a3b8');
new Chart(ctx, {
type: 'bar',
data: {
labels: classes,
datasets: [{
data: counts,
backgroundColor: colors.map(c => c + 'cc'),
borderColor: colors,
borderWidth: 1,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#94a3b8', font: { size: 12, weight: 600 } }, grid: { display: false } },
y: { ticks: { color: '#64748b' }, grid: { color: 'rgba(255,255,255,.06)' }, title: { display: true, text: 'Count', color: '#94a3b8' } },
},
},
});
}
function heatColor(val) {
const t = Math.min(Math.max(val, 0), 1);
if (t < 0.01) return 'transparent';
const r = Math.round(59 + t * 100);
const g = Math.round(130 - t * 40);
const b = Math.round(246);
const a = 0.15 + t * 0.65;
return `rgba(${r},${g},${b},${a})`;
}
function renderHeatmap() {
const el = document.getElementById('heatmap-container');
const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0);
const groupNames = {
0: 'Position', 3: 'Content Stats', 12: 'Formatting', 28: 'Formula',
32: 'Content Type', 44: 'Keywords', 55: 'Merge', 56: 'Textual/Punctuation', 71: 'TUTA Numerical',
75: 'Column/Row Context', 83: 'TableSense Cohesion',
};
let html = '<table class="heatmap"><thead><tr><th>Feature</th>';
classes.forEach(c => { html += `<th style="color:${CLASS_COLORS[c]||'#94a3b8'}">${c}</th>`; });
html += '<th style="color:var(--text-4)">Global</th></tr></thead><tbody>';
DATA.features.forEach(f => {
if (groupNames[f.index] !== undefined) {
html += `<tr><td colspan="${classes.length + 2}" class="group-label">${groupNames[f.index]}</td></tr>`;
}
html += `<tr><td class="feature-name">${f.name}</td>`;
classes.forEach(cls => {
const ci = f.classes[cls];
const val = ci ? ci.mean : 0;
html += `<td style="background:${heatColor(val)};color:${val > 0.5 ? 'var(--text-0)' : 'var(--text-2)'}" title="${cls}: mean=${val.toFixed(4)}, prev=${ci ? ci.prevalence.toFixed(3) : 0}">${val.toFixed(3)}</td>`;
});
html += `<td style="color:var(--text-4)">${f.global_mean.toFixed(3)}</td></tr>`;
});
html += '</tbody></table>';
el.innerHTML = html;
}
document.getElementById('group-select').addEventListener('change', renderComparisonChart);
document.getElementById('mode-select').addEventListener('change', renderComparisonChart);
loadData();
</script>
</body>
</html>