ControlRL / app /src /content /embeds /feature-frequency.html
seonglae-holistic's picture
fix: replace fabricated feature frequency data with real paper data
1c6aea9
Raw
History Blame Contribute Delete
15.3 kB
<!--
Feature Selection Frequency (Zipf-like)
Horizontal bar chart showing the top-10 most frequently selected SAE features
by the CRL policy on GSM8K layer 20. Data from feature_analysis_20.json "success" category.
Feature IDs link to Neuronpedia.
-->
<div class="feature-frequency"></div>
<style>
.feature-frequency {
position: relative;
font-family: system-ui, -apple-system, sans-serif;
width: 100%;
}
.feature-frequency .chart-card {
background: var(--page-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
}
.feature-frequency .chart-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 8px;
}
.feature-frequency .chart-title {
font-size: 15px;
font-weight: 700;
color: var(--text-color);
}
.feature-frequency .chart-subtitle {
font-size: 12px;
color: var(--muted-color);
font-weight: 500;
}
.feature-frequency svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.feature-frequency .axis path { stroke: none; }
.feature-frequency .axis line { stroke: var(--grid-color); }
.feature-frequency .axis text { fill: var(--tick-color); font-size: 11px; }
.feature-frequency .grid line { stroke: var(--grid-color); stroke-dasharray: 2, 3; }
.feature-frequency .grid path { stroke: none; }
.feature-frequency .bar-rect {
cursor: pointer;
transition: opacity 0.15s ease;
}
.feature-frequency .bar-rect:hover { opacity: 0.82; }
.feature-frequency .bar-count {
font-size: 11px;
font-weight: 600;
fill: var(--text-color);
pointer-events: none;
}
.feature-frequency .y-label {
font-size: 11px;
fill: var(--text-color);
cursor: pointer;
text-decoration: none;
}
.feature-frequency .y-label:hover {
fill: #4F46E5;
text-decoration: underline;
}
.feature-frequency .y-desc {
font-size: 10px;
fill: var(--muted-color);
pointer-events: none;
}
.feature-frequency .axis-label {
fill: var(--text-color);
font-size: 12px;
font-weight: 600;
}
.feature-frequency .d3-tooltip {
position: absolute;
top: 0;
left: 0;
transform: translate(-9999px, -9999px);
pointer-events: none;
padding: 10px 14px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
border: 1px solid var(--border-color);
background: var(--surface-bg);
color: var(--text-color);
box-shadow: 0 8px 32px rgba(0,0,0,.22), 0 2px 8px rgba(0,0,0,.10);
backdrop-filter: saturate(1.12) blur(8px);
-webkit-backdrop-filter: saturate(1.12) blur(8px);
opacity: 0;
transition: opacity 0.15s ease;
z-index: 20;
max-width: 260px;
}
.feature-frequency .entropy-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid var(--border-color);
font-size: 12px;
font-weight: 600;
color: var(--text-color);
background: var(--surface-bg, var(--page-bg));
}
.feature-frequency .entropy-badge .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4F46E5;
}
@media (max-width: 600px) {
.feature-frequency .chart-card {
padding: 14px;
}
.feature-frequency .y-label {
font-size: 10px;
}
.feature-frequency .y-desc {
font-size: 9px;
}
}
</style>
<script>
(function() {
var ensureD3 = function(cb) {
if (window.d3 && typeof window.d3.select === 'function') return cb();
var 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);
}
var onReady = function() {
if (window.d3 && typeof window.d3.select === 'function') cb();
};
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
var bootstrap = function() {
var candidates = Array.from(document.querySelectorAll('.feature-frequency'))
.filter(function(el) { return !el.dataset.mounted; });
var container = candidates[candidates.length - 1];
if (!container) return;
container.dataset.mounted = 'true';
container.style.position = 'relative';
var d3sel = window.d3;
/* ── Data (real from feature_analysis_20.json "success" category) ── */
var data = [
{rank: 1, feature: 2198, count: 699, label: "summaries, reflections, or clarifications"},
{rank: 2, feature: 6787, count: 644, label: "cellular biology and histology terms"},
{rank: 3, feature: 5890, count: 613, label: "technical terms and specifications"},
{rank: 4, feature: 6512, count: 518, label: "instructions and steps for processes"},
{rank: 5, feature: 14802, count: 473, label: "code structures, functions, methods"},
{rank: 6, feature: 16326, count: 452, label: "comparative measurements or evaluations"},
{rank: 7, feature: 8587, count: 439, label: "procedural language, claims and benefits"},
{rank: 8, feature: 9488, count: 425, label: "API functions and documentation"},
{rank: 9, feature: 5423, count: 409, label: "parentheses and numerical data"},
{rank: 10, feature: 9276, count: 399, label: "punctuation and conversational cues"}
];
var totalCount = data.reduce(function(s, d) { return s + d.count; }, 0);
/* ── Tooltip ── */
var tip = document.createElement('div');
tip.className = 'd3-tooltip';
var tipInner = document.createElement('div');
tip.appendChild(tipInner);
container.appendChild(tip);
function showTip(event, html) {
tipInner.innerHTML = html;
var coords = d3sel.pointer(event, container);
var tipRect = tip.getBoundingClientRect();
var contW = container.getBoundingClientRect().width;
var contH = container.getBoundingClientRect().height;
var tx = coords[0] + 14;
var ty = coords[1] - 10;
if (tx + (tipRect.width || 200) > contW - 10) tx = coords[0] - (tipRect.width || 200) - 14;
if (ty + (tipRect.height || 80) > contH - 10) ty = coords[1] - (tipRect.height || 80) - 10;
if (ty < 0) ty = coords[1] + 20;
tip.style.opacity = '1';
tip.style.transform = 'translate(' + Math.round(tx) + 'px, ' + Math.round(ty) + 'px)';
}
function hideTip() {
tip.style.opacity = '0';
tip.style.transform = 'translate(-9999px, -9999px)';
}
/* ── Card ── */
var card = document.createElement('div');
card.className = 'chart-card';
container.appendChild(card);
/* Header */
var header = document.createElement('div');
header.className = 'chart-header';
header.innerHTML =
'<div>' +
'<div class="chart-title">Feature Selection Frequency</div>' +
'<div class="chart-subtitle">GSM8K \u00b7 Layer 20 \u00b7 Top-10 most selected SAE features</div>' +
'</div>' +
'<div class="entropy-badge"><span class="dot"></span>Diversity H = 6.695</div>';
card.appendChild(header);
/* SVG container */
var svgWrap = document.createElement('div');
svgWrap.style.position = 'relative';
card.appendChild(svgWrap);
var svg = d3sel.select(svgWrap).append('svg');
var defs = svg.append('defs');
/* Gradient definition */
var gradId = 'ff-grad-' + Math.random().toString(36).slice(2);
var grad = defs.append('linearGradient')
.attr('id', gradId)
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '0%').attr('y2', '100%');
grad.append('stop').attr('offset', '0%').attr('stop-color', '#4F46E5');
grad.append('stop').attr('offset', '100%').attr('stop-color', '#a3a3a3');
var gRoot = svg.append('g');
var gGrid = gRoot.append('g').attr('class', 'grid');
var gBars = gRoot.append('g').attr('class', 'bars');
var gAxes = gRoot.append('g').attr('class', 'axes');
var hasAnimated = false;
/* ── Color interpolation per rank ── */
function barColor(rank) {
var t = (rank - 1) / (data.length - 1);
return d3sel.interpolateRgb('#4F46E5', '#a3a3a3')(t);
}
/* ── Neuronpedia URL ── */
function neuronpediaUrl(featureId) {
return 'https://neuronpedia.org/gemma-2-2b/20-gemmascope-res-16k/' + featureId;
}
/* ── Render ── */
function render(animate) {
var containerWidth = container.clientWidth || 700;
var totalWidth = Math.max(340, Math.round(containerWidth));
/* Responsive margin: narrower left margin on mobile */
var isNarrow = totalWidth < 500;
var margin = {
top: 8,
right: 48,
bottom: 40,
left: isNarrow ? 130 : 200
};
var innerWidth = totalWidth - margin.left - margin.right;
var barHeight = isNarrow ? 22 : 26;
var barPadding = isNarrow ? 4 : 6;
var totalBarArea = data.length * (barHeight + barPadding) - barPadding;
var totalHeight = margin.top + totalBarArea + margin.bottom;
svg.attr('viewBox', '0 0 ' + totalWidth + ' ' + totalHeight)
.attr('preserveAspectRatio', 'xMidYMid meet');
gRoot.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
/* Scales */
var xMax = d3sel.max(data, function(d) { return d.count; });
var xScale = d3sel.scaleLinear()
.domain([0, xMax * 1.08])
.range([0, innerWidth]);
var yScale = d3sel.scaleBand()
.domain(data.map(function(d) { return d.rank; }))
.range([0, totalBarArea])
.paddingInner(barPadding / (barHeight + barPadding))
.paddingOuter(0);
/* Grid lines */
gGrid.selectAll('*').remove();
gGrid.append('g')
.call(d3sel.axisBottom(xScale)
.ticks(isNarrow ? 4 : 6)
.tickSize(totalBarArea)
.tickFormat('')
)
.call(function(g) { g.select('.domain').remove(); });
/* ── Bars ── */
var barGroups = gBars.selectAll('g.bar-group')
.data(data, function(d) { return d.rank; });
var barEnter = barGroups.enter().append('g').attr('class', 'bar-group');
barEnter.append('rect').attr('class', 'bar-rect');
barEnter.append('text').attr('class', 'bar-count');
var barMerge = barEnter.merge(barGroups);
barMerge.attr('transform', function(d) {
return 'translate(0,' + yScale(d.rank) + ')';
});
var rects = barMerge.select('.bar-rect')
.attr('y', 0)
.attr('height', yScale.bandwidth())
.attr('rx', 4)
.attr('ry', 4)
.attr('fill', function(d) { return barColor(d.rank); });
if (animate) {
rects
.attr('x', 0)
.attr('width', 0)
.transition()
.duration(800)
.delay(function(d, i) { return i * 40; })
.ease(d3sel.easeCubicOut)
.attr('width', function(d) { return xScale(d.count); });
} else {
rects
.attr('x', 0)
.attr('width', function(d) { return xScale(d.count); });
}
/* Hover */
rects
.on('mousemove', function(event, d) {
var pct = ((d.count / totalCount) * 100).toFixed(1);
var html =
'<div style="font-weight:700;margin-bottom:2px;">F' + d.feature + '</div>' +
'<div style="font-size:11px;color:var(--muted-color);margin-bottom:6px;">' + d.label + '</div>' +
'<div style="display:flex;gap:16px;">' +
'<div><span style="font-size:10px;color:var(--muted-color);">Count</span><br><strong style="font-size:16px;">' + d.count + '</strong></div>' +
'<div><span style="font-size:10px;color:var(--muted-color);">Share</span><br><strong style="font-size:16px;">' + pct + '%</strong></div>' +
'<div><span style="font-size:10px;color:var(--muted-color);">Rank</span><br><strong style="font-size:16px;">#' + d.rank + '</strong></div>' +
'</div>';
showTip(event, html);
})
.on('mouseleave', function() { hideTip(); })
.on('click', function(event, d) {
window.open(neuronpediaUrl(d.feature), '_blank', 'noopener');
});
/* Count labels at end of bar */
var countLabels = barMerge.select('.bar-count')
.attr('y', yScale.bandwidth() / 2)
.attr('dy', '0.35em')
.text(function(d) { return d.count; });
if (animate) {
countLabels
.attr('x', 4)
.attr('opacity', 0)
.transition()
.duration(400)
.delay(function(d, i) { return i * 40 + 600; })
.attr('x', function(d) { return xScale(d.count) + 6; })
.attr('opacity', 1);
} else {
countLabels
.attr('x', function(d) { return xScale(d.count) + 6; })
.attr('opacity', 1);
}
barGroups.exit().remove();
/* ── Y-axis labels (feature IDs + descriptions) ── */
gAxes.selectAll('*').remove();
data.forEach(function(d) {
var yPos = yScale(d.rank) + yScale.bandwidth() / 2;
/* Feature ID as clickable link */
var link = gAxes.append('a')
.attr('href', neuronpediaUrl(d.feature))
.attr('target', '_blank')
.attr('rel', 'noopener');
link.append('text')
.attr('class', 'y-label')
.attr('x', -8)
.attr('y', yPos)
.attr('dy', isNarrow ? '0.35em' : '-0.15em')
.attr('text-anchor', 'end')
.attr('font-weight', 600)
.text('F' + d.feature);
/* Description below feature ID (only on wider viewports) */
if (!isNarrow) {
gAxes.append('text')
.attr('class', 'y-desc')
.attr('x', -8)
.attr('y', yPos)
.attr('dy', '1.0em')
.attr('text-anchor', 'end')
.text(d.label);
}
});
/* X-axis */
gAxes.append('g')
.attr('class', 'axis x-axis')
.attr('transform', 'translate(0,' + totalBarArea + ')')
.call(d3sel.axisBottom(xScale).ticks(isNarrow ? 4 : 6));
gAxes.append('text')
.attr('class', 'axis-label')
.attr('text-anchor', 'middle')
.attr('x', innerWidth / 2)
.attr('y', totalBarArea + 34)
.text('Selection Count');
}
/* ── Initial render (no animation) then observe for scroll-in ── */
render(false);
var obs = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting && !hasAnimated) {
hasAnimated = true;
render(true);
obs.disconnect();
}
});
}, { threshold: 0.15 });
obs.observe(container);
/* Resize */
if (window.ResizeObserver) {
new ResizeObserver(function() {
if (hasAnimated) render(false);
}).observe(container);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { ensureD3(bootstrap); }, { once: true });
} else {
ensureD3(bootstrap);
}
})();
</script>