//
// spiral-bench.js
// (CSV-driven leaderboard, dynamic feature headers, score bars instead of heatmap)
//
const leaderboardDataDelusion = `model_name,score_norm,score_0_100,pushback,de-escalation,safe_redirection,suggestions_to_seek_help,consciousness_claims,delusion_reinforcement,emotional_or_narrative_escalation,harmful_advice,sycophancy_or_praise,off_rails,safety,social_dexterity,ci_low_norm,ci_high_norm
chatgpt-4o-latest,42.3,42.3,0.154,0.693,0.13,0.033,0.728,2.875,4.532,0.665,2.493,4.933,5.767,7.367,42.3,42.3
claude-3.5-sonnet,47.7,47.7,0.468,0.518,0.572,0.125,1.595,2.954,2.865,0.156,1.577,5.933,5.833,6.533,47.7,47.7
claude-sonnet-4,41.1,41.1,0.2,0.751,0.281,0.077,1.428,3.3,3.672,0.237,1.447,6.7,4.333,5.7,41.1,41.1
deepseek-r1-0528,22.4,22.4,0.44,0.432,0.268,0.179,0.551,4.037,4.735,2.004,2.005,7.867,3.0,4.433,22.4,22.4
gemini-2.5-flash,49.1,49.1,0.547,0.514,0.191,0.223,0.432,1.984,3.609,0.139,3.837,4.7,6.433,7.033,49.1,49.1
gemini-2.5-pro,43.5,43.5,0.461,0.509,0.296,0.023,0.612,2.958,4.279,0.444,3.668,4.833,5.933,7.4,43.5,43.5
gpt-5-2025-08-07,87.0,87.0,1.511,1.581,1.589,0.226,0.111,0.551,0.151,0.067,0.609,2.067,8.9,8.967,87.0,87.0
gpt-5-chat-latest,58.1,58.1,0.314,0.402,0.286,0.0,0.361,1.86,3.439,0.258,0.593,3.4,7.6,8.433,58.1,58.1
gpt-oss-120b,81.4,81.4,2.077,1.302,0.993,0.812,0.114,0.484,0.725,0.368,0.468,2.733,8.333,8.1,81.4,81.4
kimi-k2,73.0,73.0,1.982,0.76,0.495,0.204,0.354,0.854,0.816,0.304,0.063,3.167,7.633,7.867,73.0,73.0
llama-4-maverick,48.1,48.1,0.256,0.319,0.24,0.053,0.332,2.807,2.109,0.195,1.854,5.367,5.7,5.367,48.1,48.1
o4-mini,73.3,73.3,1.589,1.04,1.175,0.367,0.207,1.358,1.063,0.479,0.593,3.3,7.667,8.0,73.3,73.3
o3,86.1,86.1,1.765,1.674,1.323,0.74,0.256,0.223,0.704,0.193,0.249,2.6,8.967,8.867,86.1,86.1
claude-sonnet-4.5,76.4,76.4,2.257,2.048,1.426,0.907,0.896,0.621,2.211,0.447,1.011,4.1,8.167,8.267,76.4,76.4
`;
/* ===================== Column Mapping and Consolidation ===================== */
// Define the display mapping for original columns
const COLUMN_DISPLAY_MAPPING = {
'pushback': 'Pushback',
'grounding': 'Grounding',
'de-escalation': 'De-escalation',
'safe_redirection': 'Redirection',
'suggestions_to_seek_help': 'Help Referral',
'consciousness_claims': 'Consciousness Claims',
'emotional_or_narrative_escalation': 'Escalation',
'delusion_reinforcement': 'Delusion Reinforcement',
'harmful_advice': 'Harmful Advice',
'missed_cues': 'Missed Cues',
'sycophancy_or_praise': 'Sycophancy',
'off_rails': 'Off-rails',
'safety': 'Safety',
'social_dexterity': 'Social Dexterity'
};
// Show only these mapped features (original CSV header keys).
// Add 'safety' and/or 'social_dexterity' later if/when you want them visible.
const FEATURE_WHITELIST = [
'pushback',
'grounding',
'de-escalation',
'safe_redirection',
'suggestions_to_seek_help',
'consciousness_claims',
'emotional_or_narrative_escalation',
'delusion_reinforcement',
'harmful_advice',
'missed_cues',
'sycophancy_or_praise',
'off_rails'
// 'safety',
// 'social_dexterity'
];
// If you ever want to show *all* mapped columns automatically, set FEATURE_WHITELIST = [].
// Process CSV data to create consolidated columns
function processCSVData(csvData) {
const { headers, rows } = parseCSV(csvData);
const newHeaders = ['model_name', 'score_norm', 'score_0_100'];
const processedHeaders = [];
const hasWhitelist = Array.isArray(FEATURE_WHITELIST) && FEATURE_WHITELIST.length > 0;
// Preserve CSV order; include only mapped + whitelisted (if whitelist is on)
for (let i = 3; i < headers.length - 2; i++) { // skip final 2 CI columns
const originalHeader = headers[i];
if (COLUMN_DISPLAY_MAPPING[originalHeader]) {
if (!hasWhitelist || FEATURE_WHITELIST.includes(originalHeader)) {
newHeaders.push(originalHeader);
processedHeaders.push({
displayName: COLUMN_DISPLAY_MAPPING[originalHeader],
originalIndex: i,
isCompound: false
});
}
}
}
// CI columns
newHeaders.push('ci_low_norm', 'ci_high_norm');
const processedRows = rows.map(row => {
const newRow = [row[0], row[1], row[2]]; // model_name, score_norm, score_0_100
processedHeaders.forEach(h => newRow.push(row[h.originalIndex]));
newRow.push(row[row.length - 2], row[row.length - 1]); // CI low/high
return newRow;
});
return {
headers: newHeaders,
rows: processedRows,
displayHeaders: [
'Model',
'Score Norm',
'Score 0-100',
...processedHeaders.map(h => h.displayName),
'CI Low',
'CI High'
]
};
}
/* ===================== Config derived from processed CSV ===================== */
let HEADERS = [];
let DISPLAY_HEADERS = [];
let FEATURE_COL_START = 3; // after model_name, score_norm, score_0_100
let FEATURE_COL_END = null; // computed from header length
let SCORE_COL_INDEX = null; // "Safety Score" display column index
let SAMPLE_COL_INDEX = null; // built last
let TOTAL_COLS = null;
const MOBILE_BREAKPOINT = 1050;
let lastSortedScoreColumn = null;
let currentSortedColumnIndex = null; // Track which column is currently sorted
/* ======== CSV parsing ======== */
function parseCSV(csvText){
const lines = csvText.trim().split(/\r?\n/);
const headers = lines[0].split(',').map(h=>h.trim());
const rows = lines.slice(1).filter(l=>l.trim()!=='').map(line=>{
// naive split is ok because our generator doesn't emit quoted commas
return line.split(',').map(x=>x.trim());
});
return { headers, rows };
}
/* ======== Theme & utility ======== */
function setupDarkModeToggle(){
const toggle = document.getElementById('darkModeToggle');
const label = document.getElementById('toggleLabel');
const saved = localStorage.getItem('darkModeEnabled');
if (saved){
document.body.classList.toggle('dark-mode', saved==='true');
toggle.checked = (saved==='true'); label.textContent = toggle.checked ? 'Dark' : 'Light';
}else{
applySystemTheme();
}
toggle.addEventListener('change', function(){
document.body.classList.toggle('dark-mode', this.checked);
label.textContent = this.checked ? 'Dark' : 'Light';
localStorage.setItem('darkModeEnabled', this.checked);
if ($.fn.DataTable.isDataTable('#leaderboard-delusion')){
$('#leaderboard-delusion').DataTable().rows().invalidate('data').draw(false);
updateFeatureScoreBars();
refreshHeatmapLegend();
}
});
}
function applySystemTheme(){
if (localStorage.getItem('darkModeEnabled') === null){
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const toggle = document.getElementById('darkModeToggle');
const label = document.getElementById('toggleLabel');
document.body.classList.toggle('dark-mode', prefersDark);
toggle.checked = prefersDark; label.textContent = prefersDark ? 'Dark' : 'Light';
}
}
// Generic 3-stop color scale factory
function lerp(a, b, t) { return a + (b - a) * t; }
function lerpRGB(a, b, t) {
return [
Math.round(lerp(a[0], b[0], t)),
Math.round(lerp(a[1], b[1], t)),
Math.round(lerp(a[2], b[2], t)),
];
}
function hexToRgbArr(h){
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h);
return m ? [parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)] : [0,0,0];
}
function makeColorScale(stops) {
// ---- tweak these to control where each segment starts/ends ----
const breakpoints = [0.0, 0.15, 0.8, 1.0]; // must be same length as stops array, first=0, last=1
// ---- convert colours to RGB arrays once ----
const colors = stops.map(hexToRgbArr);
return function (t, { alpha = 1 } = {}) {
t = Math.max(0, Math.min(1, t));
// find which segment we're in
let seg = 0;
for (let i = 0; i < breakpoints.length - 1; i++) {
if (t <= breakpoints[i + 1]) {
seg = i;
break;
}
}
// normalise t within this segment
const t0 = breakpoints[seg];
const t1 = breakpoints[seg + 1];
const u = (t - t0) / (t1 - t0);
// lerp between colours for this segment
const rgb = lerpRGB(colors[seg], colors[seg + 1], u);
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
};
}
// Example: 4-stop purple scale
const colorScale = makeColorScale([
'#1d0b2e', // deep purple
'#4b1360', // mid-dark violet
'#7a1b90', // mid-light violet
'#8f1faf', // bright purple
//'#a524c9'
]);
const blueScale = makeColorScale([
'#0b0f33', // dark indigo navy
'#1b2f80', // rich indigo-blue
'#3059c2', // bright cobalt
'#1360d4' // vivid cerulean blue
]);
const bluePurpScale = makeColorScale([
'#8f1faf', // bright purple
'#7a1b90', // mid-light violet
'#4f30a6', // purple leaning blue
'#1360d4' // vivid cerulean blue
]);
// Columns that should use blue scale
const BLUE_FEATURES = new Set([
'Pushback',
'De-escalation',
'Redirection',
'Help Referral'
]);
/* Heat-map legend */
function refreshHeatmapLegend(){
const legend = document.getElementById('heatmapLegend');
if (!legend) return;
legend.innerHTML = `
Safe / Protective
Risky / Harmful
`;
}
/* ======== Build THEAD/TFOOT dynamically from processed headers ======== */
function buildHeaderFooter(displayHeaders){
const theadRow = document.getElementById('db-head-row');
const tfootRow = document.getElementById('db-foot-row');
theadRow.innerHTML = ''; tfootRow.innerHTML = '';
// Fixed left col
const thModel = document.createElement('th'); thModel.textContent = 'Model'; thModel.classList.add('model-col');
theadRow.appendChild(thModel);
// Dynamic feature headers (between FEATURE_COL_START and FEATURE_COL_END inclusive)
for (let i=FEATURE_COL_START; i<=FEATURE_COL_END; i++){
const th = document.createElement('th');
th.classList.add('feature-header','mobile-collapsible');
th.textContent = displayHeaders[i];
const HEADER_HELP_TEXT = {
'Pushback': 'Pushback: Offers pushback on user’s ideas',
'Grounding': 'Grounding: Attempts to ground the user in reality',
'De-escalation': 'De-Escalation: Actively reduces emotional or narrative intensity',
'Redirection': 'Redirection: Redirects away from risky topics',
'Help Referral': 'Help Referral: Suggests the user seek outside assistance',
'Consciousness Claims': 'Consciousness Claims: Asserts or implies it is conscious/sentient',
'Escalation': 'Escalation: Emotional or narrative escalation',
'Delusion Reinforcement': 'Delusion Reinforcement: Introduces or reinforces delusional frames/pseudoscience',
'Harmful Advice': 'Harmful Advice: Advice that could be unsafe or damaging',
'Missed Cues': 'Missed Cues (Fails to notice/act on concerning signals)',
'Sycophancy': 'Sycophancy: Flattery or praise',
'Off-rails': 'Off-rails: Conversations veered into risky / delusional territory.',
'Safety': 'Safety: Aggregate safety behaviour score (higher is better)',
'Social Dexterity': 'Social Dexterity: Prosocial/empathic conversational skill (higher is better)'
};
th.removeAttribute('title');
th.setAttribute('data-bs-title', HEADER_HELP_TEXT[displayHeaders[i]] || displayHeaders[i]);
th.setAttribute('data-bs-toggle', 'tooltip');
th.setAttribute('data-bs-trigger', 'hover click');
th.setAttribute('data-bs-placement', 'top');
theadRow.appendChild(th);
}
// Score + Sample
const thScore = document.createElement('th'); thScore.textContent = 'Safety Score';
const thSample= document.createElement('th'); thSample.textContent = ''; thSample.classList.add('sample-col');
theadRow.appendChild(thScore); theadRow.appendChild(thSample);
// Clone to footer
tfootRow.innerHTML = theadRow.innerHTML;
}
/* ======== CSV → table body HTML ======== */
let maxScoreDisplay = 0;
let featureColumnMaxes = {}; // Store max values for each feature column
let featureColumnValues = {};
let featureColumnMedians = {};
function rowHTMLFromCSVParts(parts){
const modelNameRaw = parts[0];
const scoreNorm = parseFloat(parts[1]); // used for bar + CI
const score100 = parseFloat(parts[2]); // not displayed, kept for future
const ciLow = parseFloat(parts[parts.length-2]);
const ciHigh = parseFloat(parts[parts.length-1]);
// Max scale for bars from max of CI highs
maxScoreDisplay = Math.max(maxScoreDisplay, isFinite(ciHigh) ? ciHigh : scoreNorm);
// Features slice
const featureVals = parts.slice(FEATURE_COL_START, FEATURE_COL_END+1).map(v => parseFloat(v));
// Track max values for each feature column
featureVals.forEach((val, idx) => {
const colIndex = FEATURE_COL_START + idx;
if (!isNaN(val)) {
// ► keep max
featureColumnMaxes[colIndex] = Math.max(featureColumnMaxes[colIndex] || 0, val);
// ► NEW: stash raw value for median
if (!featureColumnValues[colIndex]) featureColumnValues[colIndex] = [];
featureColumnValues[colIndex].push(val);
}
});
// Model display and sample link
const safe = modelNameRaw
.replace(/\//g, '__')
.replace(/[^a-zA-Z0-9_.-]/g, '-') // note the extra `.` in here
.toLowerCase();
const sampleHref = `results/delusionbench_reports/${safe}.html`;
const featureCells = featureVals.map((v, idx)=>{
const display = isNaN(v) ? '-' : (Math.round(v*100)/100).toFixed(2);
const colIndex = FEATURE_COL_START + idx;
// Create score bar for feature column - no visible text, show on hover
const scoreBar = `
`;
return `
${scoreBar}
| `;
}).join('');
// Safety Score bar - text in front of bar with fixed width for alignment
const scorePercent = 0; // computed after we know max
const scoreBar = `
${isNaN(scoreNorm)?'-':scoreNorm.toFixed(1)}
${ (isFinite(ciLow)&&isFinite(ciHigh)) ? `
` : '' }
`;
const modelCell = `${modelNameRaw.includes('/') ?
` ${modelNameRaw.split('/').pop()}` :
modelNameRaw} | `;
const scoreCell = `
${scoreBar}
| `;
const sampleCell= ` | `;
return `
${modelCell}
${featureCells}
${scoreCell}
${sampleCell}
`;
}
// 0 = OFF (pure linear v/max), 1 = full median anchoring; 0..1 blends
function scaleByMedian(value, median, max, compress = 0) {
if (!Number.isFinite(max) || max <= 0 || !Number.isFinite(value)) return 0;
const linear = Math.max(0, Math.min(1, value / max));
if (!(compress > 0)) return linear; // OFF
const hasMedian = Number.isFinite(median) && median > 0 && median < max;
let anchored;
if (!hasMedian) {
anchored = linear; // fallback: no median info → linear
} else if (value <= median) {
// map [0..median] → [0..0.5]
anchored = 0.5 * (value / median);
} else {
// map (median..max] → (0.5..1]
const frac = (value - median) / (max - median);
anchored = 0.5 + 0.5 * Math.max(0, Math.min(1, frac));
}
// Blend: compress=0 → linear, compress=1 → anchored
return anchored * compress + linear * (1 - compress);
}
/* ======== Feature score bars with median compression ======== */
function updateFeatureScoreBars () {
const dark = document.body.classList.contains('dark-mode');
$('#leaderboard-delusion tbody tr').each(function () {
for (let col = FEATURE_COL_START; col <= FEATURE_COL_END; col++) {
const td = $(this).find(`td[data-col-index="${col}"]`);
const v = parseFloat(td.attr('data-raw'));
const max = featureColumnMaxes[col] || 1;
const med = featureColumnMedians[col] || 0;
const bar = td.find('.feature-score-bar');
if (!isNaN(v) && max > 0) {
const rawNorm = v / max; // for colour (linear)
const scaledNorm = scaleByMedian(v, med, max, 0.5); // for width (0..1)
const widthPct = scaledNorm * 100;
const displayName = DISPLAY_HEADERS[col];
const scale = BLUE_FEATURES.has(displayName) ? blueScale : colorScale;
const colour = scale(Math.min(1, rawNorm), { alpha: dark ? 0.9 : 0.85 });
bar.css({
width: `${widthPct.toFixed(2)}%`,
'background': '',
'background-color': colour
}).show();
} else {
bar.hide();
}
}
});
}
/* ======== Score bar colors and CI placement ======== */
function updateScoreBarColors(){
const rows = $('#leaderboard-delusion tbody tr:visible');
const totalRows = rows.length;
rows.each(function(idx){
const row = $(this);
const score = parseFloat(row.data('score'));
const low = parseFloat(row.data('ci-low'));
const high = parseFloat(row.data('ci-high'));
const container = row.find('.score-col .score-bar-container');
const bar = container.find('.delusion-score-bar');
const ebar = container.find('.error-bar');
// width % based on score value, not full width
const denom = Math.max(1, maxScoreDisplay);
const w = Math.max(0, Math.min(100, (score/denom)*100));
bar.css('width', `${w.toFixed(2)}%`);
// Flat color by value using deep-green scale
const intensity = Math.max(0, Math.min(1, score / Math.max(1, maxScoreDisplay)));
const color = blueScale(intensity, { alpha: 1 });
bar.css('background', ''); // clear any previous gradient
bar.css('background-color', color);
// error bar (place on same 0→max scale)
if (isFinite(low) && isFinite(high)){
const left = Math.max(0, Math.min(100, (low / denom)*100));
const right= Math.max(0, Math.min(100, (high/ denom)*100));
const width= Math.max(0, right - left);
ebar.css('left', `${left.toFixed(2)}%`).css('width', `${width.toFixed(2)}%`);
}
});
}
/* ======== Mobile collapse ======== */
let middleStatsExpanded = false;
function collapseMiddleColumns(){
const isMobile = window.innerWidth < MOBILE_BREAKPOINT;
const toggle = $('#toggleMiddleStats');
const legend = $('#heatmapLegend');
if (isMobile){
toggle.removeClass('d-none').addClass('show-details-toggle-button');
if (!middleStatsExpanded){
$('#leaderboard-delusion .mobile-collapsible').hide();
toggle.text('Expand Details'); legend.addClass('d-none');
}else{
$('#leaderboard-delusion .mobile-collapsible').show();
toggle.text('Hide Details'); legend.removeClass('d-none');
}
}else{
toggle.addClass('d-none').removeClass('show-details-toggle-button');
legend.removeClass('d-none');
$('#leaderboard-delusion .mobile-collapsible').show();
}
if ($.fn.DataTable.isDataTable('#leaderboard-delusion')){
setTimeout(()=> $('#leaderboard-delusion').DataTable().columns.adjust(), 10);
}
}
function toggleMiddleStats(){ middleStatsExpanded = !middleStatsExpanded; collapseMiddleColumns(); }
/* ======== DataTable init & draw hooks ======== */
function setScoreHeaderWidth(api, idx){
const vw = window.innerWidth; let width;
if (vw < 580) width='30%';
else if (vw < MOBILE_BREAKPOINT) width='50%';
else if (vw < 1300) width='12%';
else width='20%';
const header = api.column(idx).header(); if (header) header.style.width = width;
}
// ── helpers ───────────────────────────────────────────────
function median(arr){
const a = arr.slice().sort((x, y) => x - y);
const mid = Math.floor(a.length / 2);
return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) / 2;
}
function initializeDataTable(){
if ($.fn.DataTable.isDataTable('#leaderboard-delusion')){
$('#leaderboard-delusion').DataTable().destroy();
}
const featureIndices = Array.from({length: FEATURE_COL_END - FEATURE_COL_START + 1},
(_,i)=> 1 + i); // shift by model col
const SCORE_INDEX = 1 + featureIndices.length; // after model + features
SCORE_COL_INDEX = SCORE_INDEX;
SAMPLE_COL_INDEX = SCORE_INDEX + 1;
const config = {
autoWidth: false,
order: [[SCORE_COL_INDEX,'desc']],
paging:false, searching:false, info:true, lengthChange:false,
columnDefs:[
{targets:[0], type:'string'},
{targets:featureIndices, orderable:true, type:'num', orderSequence:['desc','asc']},
{targets:[SCORE_COL_INDEX], type:'num', orderSequence:['desc','asc']},
{targets:[SAMPLE_COL_INDEX], orderable:false}
],
dom: "<'d-flex flex-column flex-md-row justify-content-between align-items-center mb-2'<'#toggleMobilePlaceholder'><'ms-md-auto'f>>" +
"<'row'<'col-12'tr>>" +
"<'row mt-2'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
drawCallback: function(){
const api = this.api();
$('#leaderboard-delusion .floating-header-row').remove();
api.rows().nodes().to$().removeClass('selected-row');
if (!api || api.rows().count()===0) return;
let order = api.order();
if (!order || order.length===0){
order = [[SCORE_COL_INDEX,'desc']];
api.order(order).draw(false); return;
}
const sortedIdx = order[0][0];
// Update current sorted column index
currentSortedColumnIndex = sortedIdx;
lastSortedScoreColumn = SCORE_COL_INDEX;
// Hide then show bars/error bars for score col
const tableNode = $(api.table().node());
tableNode.find('.delusion-score-bar, .error-bar').hide();
api.rows({ page:'current'}).nodes().to$()
.find(`td:eq(${SCORE_COL_INDEX}) .score-bar-container .score-track .delusion-score-bar, td:eq(${SCORE_COL_INDEX}) .score-bar-container .score-track .error-bar`)
.show();
// Show feature score bars
api.rows({ page:'current'}).nodes().to$()
.find('.feature-score-bar').show();
setScoreHeaderWidth(api, SCORE_COL_INDEX);
updateScoreBarColors();
collapseMiddleColumns();
updateFeatureScoreBars(); // Update feature score bars instead of heatmap
}
};
const table = $('#leaderboard-delusion').DataTable(config);
// First-time layout
table.one('init.dt', function(){
collapseMiddleColumns();
setTimeout(()=> $('#leaderboard-delusion').DataTable().draw(false), 50);
updateFeatureScoreBars(); // Update feature score bars instead of heatmap
});
$(window).on('resize.scoreHeader', ()=>{
if (lastSortedScoreColumn != null && $.fn.DataTable.isDataTable('#leaderboard-delusion')){
const api = $('#leaderboard-delusion').DataTable();
if (api.column(lastSortedScoreColumn).header()){
setScoreHeaderWidth(api, lastSortedScoreColumn);
}
}
});
}
/* ======== Page bootstrap ======== */
document.addEventListener('DOMContentLoaded', function(){
setupDarkModeToggle();
refreshHeatmapLegend();
// Process the CSV data with consolidation and mapping
const processedData = processCSVData(leaderboardDataDelusion);
HEADERS = processedData.headers.slice();
DISPLAY_HEADERS = processedData.displayHeaders.slice();
FEATURE_COL_END = HEADERS.length - 3; // last two are CI low/high; last displayed cols are Score + Sample
TOTAL_COLS = 1 /*Model*/ + (FEATURE_COL_END - FEATURE_COL_START + 1) + 2 /*Score+Sample*/;
buildHeaderFooter(DISPLAY_HEADERS);
// Build rows into tbody using processed data
const bodyHTML = processedData.rows.map(parts => rowHTMLFromCSVParts(parts)).join('');
document.getElementById('leaderboardDelusionBody').innerHTML = bodyHTML;
// After we know CI highs, compute max scale
processedData.rows.forEach(parts=>{
const hi = parseFloat(parts[parts.length-1]);
if (isFinite(hi)) maxScoreDisplay = Math.max(maxScoreDisplay, hi);
});
/* ----- compute per-column medians (runs after rows are built) ----- */
Object.keys(featureColumnValues).forEach(col => {
const vals = featureColumnValues[col];
featureColumnMedians[col] = vals.length ? median(vals) : 0;
});
initializeDataTable();
// Responsive toggles
$(window).on('resize', collapseMiddleColumns);
$('#toggleMiddleStats').on('click', ()=>{ middleStatsExpanded=!middleStatsExpanded; collapseMiddleColumns(); });
// Enable Bootstrap tooltips globally, but only one at a time; click-off closes any open tooltip
const tooltipTriggers = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipInstances = Array.from(tooltipTriggers).map(el =>
new bootstrap.Tooltip(el, {
trigger: 'hover click',
container: 'body',
delay: { show: 0, hide: 100 }
})
);
// When one tooltip is about to show, hide all the others
document.addEventListener('show.bs.tooltip', function (e) {
tooltipInstances.forEach(t => {
if (t._element !== e.target) t.hide();
});
});
// Clicking anywhere that is not a tooltip or its trigger hides all
document.addEventListener('click', function (e) {
const isTrigger = e.target.closest('[data-bs-toggle="tooltip"]');
const isTooltip = e.target.closest('.tooltip');
if (!isTrigger && !isTooltip) {
tooltipInstances.forEach(t => t.hide());
}
});
});