muttsurini's picture
Initial Push
8a34897 verified
Raw History Blame Contribute Delete
56.5 kB
'use strict';
// GCS JSON API downloads are CORS-accessible independently of bucket CORS rules.
// These are publicly released dataset archive paths documented by THESEUS.
const GCS_BUCKET='halcyon_data';
const GCS_PREFIX='multihop_ds/datasets/';
function gcsDownloadUrl(path){
return `https://storage.googleapis.com/download/storage/v1/b/${GCS_BUCKET}/o/${encodeURIComponent(GCS_PREFIX+path)}?alt=media`;
}
const GRAPH_DATASETS={
kinship:{label:'Kinship',repository:'HalcyonSolutions/Kinship',file:'kg/orig/triplets.txt',gcs:'Kinship/kinship_dataset.zip',root:'kinship_hinton'},
mquake:{label:'MQuAKE-ST',repository:'HalcyonSolutions/MQuAKE-ST',file:'kg/triplets.txt',gcs:'MQuAKE_ST/mquake_st_dataset.zip',root:'mquake_st_dataset',relations:'metadata/relation_data.csv',entities:'metadata/node_data.csv'},
metaqa:{label:'MetaQA (encoded)',repository:null,file:'kg/triplets.txt',gcs:'MetaQA/metaqa_dataset.zip',root:'metaqa_dataset',relations:'metadata/relation_data.csv',entities:'metadata/node_data.csv'}
};
const QA_DATASETS={
kinship:{label:'Kinship QA',repository:'HalcyonSolutions/Kinship',file:'qa/kinship_qa_nhop.csv',gcs:'Kinship/kinship_dataset.zip',root:'kinship_hinton'},
'mquake-sa':{label:'MQuAKE-ST Single Answer',repository:'HalcyonSolutions/MQuAKE-ST',file:'qa/single_answers/qa_nhop.csv',gcs:'MQuAKE_ST/mquake_st_dataset.zip',root:'mquake_st_dataset'},
'mquake-ma':{label:'MQuAKE-ST Multi Answer',repository:'HalcyonSolutions/MQuAKE-ST',file:'qa/multi_answers/qa_nhop.csv',gcs:'MQuAKE_ST/mquake_st_dataset.zip',root:'mquake_st_dataset'},
metaqa:{label:'MetaQA QA (encoded)',repository:null,file:'qa/metaqa_nhop.csv',gcs:'MetaQA/metaqa_dataset.zip',root:'metaqa_dataset'}
};
// A graph and QA collection must share their entity/relation vocabulary.
// Graph-free QA analysis is allowed for every QA source, while every graph
// permits an explicit No QA selection for graph-only analysis.
const QA_BY_GRAPH={
kinship:['kinship','none'],
mquake:['mquake-sa','mquake-ma','none'],
metaqa:['metaqa','none'],
custom:['custom','none'],
none:['kinship','mquake-sa','mquake-ma','metaqa','custom','none']
};
const QA_COMPATIBILITY_TEXT={
kinship:'Kinship QA',
mquake:'MQuAKE-ST Single Answer or Multi Answer',
metaqa:'MetaQA QA',
custom:'Custom QA',
none:'Any QA collection (QA-only mode)'
};
const $=id=>document.getElementById(id);
let activeWorker=null, latest=null, labels=new Map(), entityLabels=new Map(), running=false, relationHeader=[], nodeHeader=[];
// A page-owned backup survives terminating an actively calculating Worker.
// Blobs are structured-cloned as lightweight file handles, not sent to a server.
const remoteDownloadCache=new Map();
const remoteMetadataCache=new Map();
function formatCacheBytes(bytes){
return bytes>=1048576?`${(bytes/1048576).toFixed(1)} MiB`:`${(bytes/1024).toFixed(1)} KiB`;
}
function datasetCacheLabel(entry){
const url=entry.url;
if(entry.provider==='gcs'){
const objectMatch=url.match(/\/o\/([^?]+)/);
const path=objectMatch?decodeURIComponent(objectMatch[1]):url;
const parts=path.split('/');
const directory=parts.length>1?parts[parts.length-2]:'GCS';
return {title:`${directory} · ${parts[parts.length-1]||'archive.zip'}`,provider:'Google Cloud Storage · ZIP archive'};
}
try{
const u=new URL(url);
const parts=u.pathname.split('/').filter(Boolean);
const i=parts.indexOf('datasets');
const dataset=i>=0?parts[i+2]||'Hugging Face':'Hugging Face';
const file=parts.slice(parts.indexOf('main')+1).join('/')||parts[parts.length-1]||'file';
return {title:`${dataset} · ${decodeURIComponent(file)}`,provider:'Hugging Face · file'};
}catch(_){return {title:url.split('/').pop(),provider:'Hugging Face · file'};}
}
function stopIdleWorker(){
// The Worker has its own cache and must not reuse a page-evicted resource.
// Other page-owned Blobs remain available for restoration on the next run.
if(activeWorker && !running){activeWorker.terminate();activeWorker=null;}
}
function removeDatasetCacheEntry(key){
if(running)return;
const entry=remoteDownloadCache.get(key);
if(!entry)return;
stopIdleWorker();
remoteDownloadCache.delete(key);
// Metadata fetched separately from Hugging Face may describe this resource.
// Clear those tiny derived requests so switching/reloading never shows stale labels.
if(entry.provider==='hf')remoteMetadataCache.clear();
updateDatasetCacheStatus();
status(`Deleted cached ${datasetCacheLabel(entry).title}. Other downloaded datasets are retained.`);
}
function updateDatasetCacheStatus(){
const summary=$('download-cache-summary');
const count=remoteDownloadCache.size, extra=remoteMetadataCache.size;
const bytes=[...remoteDownloadCache.values()].reduce((sum,entry)=>sum+entry.blob.size,0);
summary.textContent=count?
`${count} downloaded ${count===1?'resource':'resources'} cached · ${formatCacheBytes(bytes)}. Hugging Face and GCS are stored separately.`:
'No downloaded dataset files cached yet.';
const container=$('download-cache-list');
container.replaceChildren();
for(const [key,entry] of remoteDownloadCache){
const line=document.createElement('div');line.className='download-cache-item';
const info=document.createElement('div');info.className='download-cache-item-info';
const title=document.createElement('div');title.className='download-cache-item-title';
title.textContent=datasetCacheLabel(entry).title;
const detail=document.createElement('div');detail.className='download-cache-item-detail';
detail.textContent=`${datasetCacheLabel(entry).provider} · ${formatCacheBytes(entry.blob.size)}`;
info.append(title,detail);
const del=document.createElement('button');del.type='button';del.className='button ghost small-button download-cache-delete';
del.textContent='Delete';del.setAttribute('aria-label',`Delete cached ${title.textContent}`);
del.disabled=running;
del.addEventListener('click',()=>removeDatasetCacheEntry(key));
line.append(info,del);container.appendChild(line);
}
$('clear-download-cache').disabled=running || (!count&&!extra);
}
function clearDatasetCache(){
if(running)return;
stopIdleWorker();
remoteDownloadCache.clear();remoteMetadataCache.clear();
updateDatasetCacheStatus();
status('Downloaded dataset cache cleared. The next analysis will fetch built-in files again.');
}
function getCachedMetadata(url){
if(!remoteMetadataCache.has(url)){
const loading=fetch(url).then(r=>{if(!r.ok)throw Error(`Metadata HTTP ${r.status}`);return r.text();});
remoteMetadataCache.set(url,loading);
loading.catch(()=>{if(remoteMetadataCache.get(url)===loading)remoteMetadataCache.delete(url);});
}
return remoteMetadataCache.get(url);
}
let dashboardAggregation='instance-micro', dashboardTarget='Answer-Entity';
const nf=new Intl.NumberFormat('en-US',{maximumFractionDigits:3});
const number=v=>Number.isFinite(v)?nf.format(v):'—';
const fixed=(v,d=3)=>Number.isFinite(v)?Number(v).toFixed(d):'—';
const pct=v=>Number.isFinite(v)?`${(v*100).toFixed(2)}%`:'—';
const sci=v=>Number.isFinite(v)?Number(v).toExponential(3):'—';
const escaped=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const sourceSelected=name=>document.querySelector(`input[name=${name}]:checked`)?.value||'none';
const hasGraph=()=>sourceSelected('graph-source')!=='none';
const hasQA=()=>sourceSelected('qa-source')!=='none';
function enforceQaCompatibility(graphId=sourceSelected('graph-source'),repair=true){
const allowed=QA_BY_GRAPH[graphId];
if(!allowed)throw new Error(`Unknown graph source: ${graphId}`);
const selected=sourceSelected('qa-source');
if(repair&&!allowed.includes(selected)){
// Preserve an intentional No QA selection. Otherwise pick the first QA
// setting compatible with the newly chosen graph.
const fallback=allowed[0];
document.querySelector(`input[name="qa-source"][value="${fallback}"]`).checked=true;
}
document.querySelectorAll('input[name="qa-source"]').forEach(input=>{
const available=allowed.includes(input.value);
input.disabled=running||!available;
const label=input.closest('.source-option');
if(label){
label.classList.toggle('source-option-unavailable',!available);
label.setAttribute('aria-disabled',String(!available));
}
});
return sourceSelected('qa-source');
}
function providerSelected(){return $('dataset-provider').value;}
function updateSourceHints(){
const gid=sourceSelected('graph-source');
const qid=enforceQaCompatibility(gid);
const customGraph=gid==='custom',customQA=qid==='custom';
const builtIn=['kinship','mquake','metaqa'].includes(gid)||['kinship','mquake-sa','mquake-ma','metaqa'].includes(qid);
const metaqa=gid==='metaqa'||qid==='metaqa';
$('builtin-provider').classList.toggle('hidden',!builtIn);
$('dataset-provider').querySelector('option[value="hf"]').disabled=metaqa;
if(metaqa)$('dataset-provider').value='gcs';
const provider=providerSelected();
$('custom-graph-options').classList.toggle('hidden',!customGraph);
$('custom-qa-options').classList.toggle('hidden',!customQA);
function datasetHint(cfg){
if(provider==='gcs')return `From <a href="${gcsDownloadUrl(cfg.gcs)}" target="_blank" rel="noopener noreferrer">Google Cloud Storage · JSON API archive ↗</a>`;
return `From <a href="https://huggingface.co/datasets/${cfg.repository}" target="_blank" rel="noopener noreferrer">${cfg.repository} · Hugging Face ↗</a>`;
}
$('graph-source-hint').innerHTML=gid==='none'?'Graph analysis disabled.':customGraph?
'Local graph files are processed only in your browser. No upload to a server.':datasetHint(GRAPH_DATASETS[gid]);
$('qa-compat-hint').textContent=gid==='none'?
'No graph selected. Any QA source is available for standalone QA analysis.':
`Compatible with ${QA_COMPATIBILITY_TEXT[gid]}. Choose No QA for graph-only analysis.`;
$('qa-source-hint').innerHTML=qid==='none'?'QA analysis disabled.':customQA?
'Local QA files are processed only in your browser. No upload to a server.':datasetHint(QA_DATASETS[qid]);
$('provider-hint').textContent=metaqa?
'MetaQA (encoded) is only available from the published GCS ZIP. The JSON API allows browser downloads without changing bucket CORS settings.':
provider==='gcs'?
'GCS downloads one publicly available ZIP archive per dataset, then extracts graph, QA, and metadata locally. This may use substantial browser memory.':
'Hugging Face loads files individually. Switch to GCS to read the published ZIP archives through the JSON API.';
}
function status(text,kind=''){$('status').textContent=text;$('status').className=`status ${kind}`;}
function progress(text,detail='',fraction=null){$('progress-wrap').classList.remove('hidden');$('progress-text').textContent=`${text}${detail?' · '+detail:''}`;
const bar=$('progress-inner');bar.style.width=Number.isFinite(fraction)?`${Math.max(0,Math.min(100,fraction*100))}%`:'15%';}
function toggleRun(value){running=value;$('analyze').disabled=value;$('cancel').classList.toggle('hidden',!value);
updateDatasetCacheStatus();
const selectors=['#opt-run-random-walk','#dataset-provider','#preset-export','#preset-import','input[name=graph-source]','input[name=qa-source]','#triples-file','#entities-file','#relations-file','#delimiter','#qa-file','#qa-split','#rw-steps','#opt-clustering','#opt-eigen','#opt-legacy','#opt-self-loops','#opt-undirected','#opt-explode-paraphrases','#relation-id-column','#relation-label-column','#node-id-column','#node-label-column','#opt-include-metadata-entities'];
for(const selector of selectors) document.querySelectorAll(selector).forEach(el=>el.disabled=value);
enforceQaCompatibility(sourceSelected('graph-source'), !value);
updateRandomWalkControls();
}
function updateRandomWalkControls(){
const enabled=$('opt-run-random-walk').checked;
$('rw-steps').disabled=running||!enabled;
$('rw-steps').closest('.rw-horizon-field')?.classList.toggle('rw-disabled',!enabled);
}
function reportError(message){status(message,'error');$('progress-wrap').classList.add('hidden');toggleRun(false);}
function builtInJob(cfg){
if(providerSelected()==='gcs')return {
mode:'archive',name:cfg.label,provider:'gcs',archiveUrl:gcsDownloadUrl(cfg.gcs),
entryPath:cfg.file,root:cfg.root,relationPath:cfg.relations||null,entityPath:cfg.entities||null,
delimiter:cfg.file.endsWith('.txt')?'\t':'auto'
};
if(!cfg.repository)throw new Error(`${cfg.label} is only available from Google Cloud Storage.`);
return {mode:'url',name:cfg.label,provider:'hf',repo:cfg.repository,
url:`https://huggingface.co/datasets/${cfg.repository}/resolve/main/${cfg.file}`,
delimiter:cfg.file.endsWith('.txt')?'\t':'auto'};
}
function graphJob(){
const kind=sourceSelected('graph-source');
if(kind==='none') return null;
if(kind==='custom'){
const file=$('triples-file').files[0];
if(!file) throw new Error('Select a local triplets file first, or disable graph analysis.');
return {mode:'file',name:file.name,file,delimiter:$('delimiter').value==='tab'?'\t':$('delimiter').value};
}
return builtInJob(GRAPH_DATASETS[kind]);
}
function qaJob(){
const kind=sourceSelected('qa-source');
if(kind==='none') return null;
if(kind==='custom'){
const file=$('qa-file').files[0];
if(!file) throw new Error('Select a local QA CSV first, or disable QA analysis.');
return {mode:'file',name:file.name,file,delimiter:'auto'};
}
return builtInJob(QA_DATASETS[kind]);
}
async function run(){
if(running)return;
try{
const gid=sourceSelected('graph-source'), qid=sourceSelected('qa-source');
if(!QA_BY_GRAPH[gid]?.includes(qid)) throw new Error('The selected QA collection is incompatible with the graph. Select a matching QA source or No QA.');
const graphSource=graphJob(), qaSource=qaJob();
if(!graphSource && !qaSource){reportError('Select at least one graph or QA source.');return;}
labels=new Map(); entityLabels=new Map();
toggleRun(true);status('Preparing analysis…');progress('Preparing');
const created=!activeWorker;
if(created)activeWorker=new Worker('stats-worker.js?v=11.3');
const worker=activeWorker;
worker.onmessage=event=>{
if(worker!==activeWorker)return;
const m=event.data;
if(m.type==='progress'){status(`${m.phase}: ${m.detail}`);progress(m.phase,m.detail,m.fraction);}
else if(m.type==='cache-entry'){
// Keep the downloaded Blob on the page so Cancel can terminate CPU work
// without losing already-fetched data. Only successful downloads arrive.
remoteDownloadCache.set(`${m.provider}:${m.url}`,{provider:m.provider,url:m.url,blob:m.blob});
updateDatasetCacheStatus();
}
else if(m.type==='cache-evict'){
remoteDownloadCache.delete(`${m.provider}:${m.url}`);
updateDatasetCacheStatus();
}
else if(m.type==='cache-status'){updateDatasetCacheStatus();}
else if(m.type==='labels'){labels=new Map(m.relations||[]);entityLabels=new Map(m.entities||[]);}
else if(m.type==='error'){reportError(m.message);}
else if(m.type==='complete'){
latest=m.result;toggleRun(false);$('progress-wrap').classList.add('hidden');
const pieces=[]; if(latest.kg) pieces.push(`${number(latest.kg.summary.triplet_rows)} triple rows`); if(latest.qa) pieces.push(`${number(latest.qa.summary.qa_rows)} QA rows`);
status(`Analysis completed in ${fixed(latest.runtime_seconds,2)} s. ${pieces.join(' · ')}.`,'success');
$('empty').classList.add('hidden');$('results').classList.remove('hidden');
render(); if(graphSource?.mode!=='archive')loadLabels().catch(()=>{});
}
};
worker.onerror=e=>{
if(worker!==activeWorker)return;
worker.terminate();activeWorker=null;
updateDatasetCacheStatus();
reportError(`Computation error: ${e.message||'Worker failed'}`);
};
// This message is queued before analyze in a new Worker. The worker
// restores downloads lazily and does not redo network requests.
if(created && remoteDownloadCache.size){
worker.postMessage({action:'seed-cache',entries:[...remoteDownloadCache.values()]});
}
worker.postMessage({action:'analyze',job:{
graphSource, qaSource,
entityFile:sourceSelected('graph-source')==='custom'?($('entities-file').files[0]||null):null,
entityIdColumn:$('node-column-wrap').classList.contains('hidden')?null:Number($('node-id-column').value),
options:{includeMetadataEntities:$('opt-include-metadata-entities').checked,clustering:$('opt-clustering').checked,eigenvector:$('opt-eigen').checked,legacy:$('opt-legacy').checked,
qaSplit:$('qa-split').value,rwSteps:$('rw-steps').value,runRandomWalk:$('opt-run-random-walk').checked,
useSelfLoops:$('opt-self-loops').checked,useUndirectedActions:$('opt-undirected').checked,explodeParaphrases:$('opt-explode-paraphrases').checked}
}});
}catch(err){reportError(err.message||String(err));}
}
function cancel(){
if(!running)return;
// Terminating the compute Worker stops synchronous graph calculations.
// Successfully downloaded resources remain in the page-owned Blob cache.
if(activeWorker){activeWorker.terminate();activeWorker=null;}
toggleRun(false);$('progress-wrap').classList.add('hidden');
status('Calculation canceled. Downloaded datasets are still cached.');
}
function parseCSVHeader(text){
const line=text.replace(/^\ufeff/,'').split(/\r?\n/).find(x=>x.trim());
if(!line)return [];
const sep=line.includes('\t')?'\t':line.includes(',')?',':';';
const cells=[];let cell='',quote=false;
for(let i=0;i<line.length;i++){
const ch=line[i];
if(ch==='"'){ if(quote&&line[i+1]==='"'){cell+='"';i++;} else quote=!quote; }
else if(ch===sep&&!quote){cells.push(cell.trim());cell='';}
else cell+=ch;
}
cells.push(cell.trim());
return cells;
}
function setColumnSelectors(headers,prefix){
const wrap=$(`${prefix}-column-wrap`), idSel=$(`${prefix}-id-column`), labelSel=$(`${prefix}-label-column`);
idSel.innerHTML=''; labelSel.innerHTML='';
if(!headers.length){wrap.classList.add('hidden');return;}
headers.forEach((h,i)=>{
const option=document.createElement('option'); option.value=String(i); option.textContent=h||`Column ${i+1}`; idSel.appendChild(option);
const other=document.createElement('option'); other.value=String(i); other.textContent=h||`Column ${i+1}`; labelSel.appendChild(other);
});
const lower=headers.map(h=>String(h).trim().toLowerCase());
const idWords=prefix==='node'?['qid','eid','node','node-id','node_id','entity','entity-id','entity_id','id']:
['property','relation','rid','id','pid','qid','eid'];
const idIndex=lower.findIndex(x=>idWords.includes(x));
const labelIndex=lower.findIndex(x=>['title','label','name','display name','display_name'].includes(x));
idSel.value=String(idIndex>=0?idIndex:0);
labelSel.value=String(labelIndex>=0?labelIndex:Math.min(1,headers.length-1));
wrap.classList.remove('hidden');
}
async function onColumnFileChange(prefix,fileId){
const file=$(fileId).files[0];
if(prefix==='node')nodeHeader=[];else relationHeader=[];
if(!file){setColumnSelectors([],prefix);return;}
const headers=parseCSVHeader(await file.text());
if(prefix==='node')nodeHeader=headers;else relationHeader=headers;
setColumnSelectors(headers,prefix);
}
async function onEntityFileChange(){
const file=$('entities-file').files[0];
if(!file){nodeHeader=[];setColumnSelectors([],'node');return;}
// Single-column, one-ID-per-line lists have no node title column.
const headers=parseCSVHeader(await file.text());
nodeHeader=headers.length>1?headers:[];
setColumnSelectors(nodeHeader,'node');
if(latest&&!running)loadLabels().catch(()=>{});
}
function parseCSVMappings(text,idIndex=null,labelIndex=null){
const lines=text.replace(/^\ufeff/,'').split(/\r?\n/).filter(x=>x.trim()); if(!lines.length) return new Map();
const sep=lines[0].includes('\t')?'\t':lines[0].includes(',')?',':';';
const parseLine=line=>{
if(sep==='\t') return line.split('\t');
const cells=[]; let cell='',quoted=false;
for(let i=0;i<line.length;i++){
const ch=line[i];
if(ch==='"'){ if(quoted&&line[i+1]==='"'){cell+='"';i++;} else quoted=!quoted; }
else if(ch===sep&&!quoted){cells.push(cell);cell='';}
else cell+=ch;
}
cells.push(cell); return cells;
};
const header=parseLine(lines[0]).map(x=>x.trim());
const lower=header.map(x=>x.toLowerCase());
let k=idIndex, t=labelIndex;
if(k===null||Number.isNaN(k)) k=lower.findIndex(x=>['property','relation','rid','id','pid','qid','eid'].includes(x));
if(t===null||Number.isNaN(t)) t=lower.findIndex(x=>['title','label','name'].includes(x));
if(k<0||t<0) return new Map();
const out=new Map();
for(const line of lines.slice(1)){
const cells=parseLine(line); const id=(cells[k]||'').trim(); const label=(cells[t]||'').trim(); if(id&&label) out.set(id,label);
}
return out;
}
async function loadLabels(){
const kind=sourceSelected('graph-source');
const getCSV=getCachedMetadata;
if(kind==='custom'){
const file=$('relations-file').files[0];
if(file){
const text=await file.text();
const idIndex=Number($('relation-id-column').value); const labelIndex=Number($('relation-label-column').value);
labels=parseCSVMappings(text,idIndex,labelIndex);
}
// Entity metadata supplies readable labels independently from graph membership.
// The worker includes extra metadata-only entities only on explicit opt-in.
const entities=$('entities-file').files[0];
if(entities && !$('node-column-wrap').classList.contains('hidden')){
const text=await entities.text();
const idIndex=Number($('node-id-column').value), labelIndex=Number($('node-label-column').value);
if(idIndex!==labelIndex) entityLabels=parseCSVMappings(text,idIndex,labelIndex);
}
} else if(kind==='mquake' && providerSelected()==='hf'){
const base=`https://huggingface.co/datasets/${GRAPH_DATASETS[kind].repository}/resolve/main/metadata/`;
const [rels,nodes]=await Promise.allSettled([getCSV(base+'relation_data.csv'),getCSV(base+'node_data.csv')]);
if(rels.status==='fulfilled')labels=parseCSVMappings(rels.value);
if(nodes.status==='fulfilled')entityLabels=parseCSVMappings(nodes.value);
}
if(latest) renderRelations();
}
const GLOSSARY={
'Unique triples':'Distinct directed (head, relation, tail) triples. Duplicate rows are counted separately.',
'Entities':'Distinct node IDs in the active graph. Metadata-only nodes are excluded unless explicitly included.',
'Relations':'Number of distinct relation types appearing in graph triples.',
'Largest WCC':'Share of graph entities in the largest weakly connected component, ignoring edge directions.',
'WCC':'Weakly connected component, a connected group when edge directions are ignored.',
'QA rows':'Number of selected QA records after applying the benchmark split filter.',
'Question families':'Number of distinct structural question families. Related wording variants may belong to the same family.',
'Instance-micro':'Each evaluation instance has equal weight, including each separately represented question variant.',
'Family-macro':'Average the result within each question family, then weight each family equally.',
'Answer (released)':'Evaluate the released reference answer entities (Answer-Entity).',
'Graph-Answer (graph-consistent)':'Evaluate graph-consistent endpoints (Graph-Answer-Entity) supplied in the QA collection. These are not independent gold answers.',
'Mean answer count':'Average number of target answer entities in the selected QA population.',
'Mean paraphrases':'Mean number of available question wordings per selected QA record.',
'RW-Ans@p-actions':'Probability that a uniform random walk over labeled graph actions reaches any target answer at exactly the selected step horizon.',
'RW-Ans@p-neighbors':'Probability that a uniform random walk over unique next-node neighbors reaches any target answer at exactly the selected step horizon.',
'RW-RefPath@p-actions':'Probability of executing a released reference path, including final self-loop padding when enabled and needed.',
'Reachable N count':'Number of distinct entities with nonzero probability after exactly N random-walk steps.',
'Reference paths':'Number of evaluated QA instances with at least one released or reconstructed reference path.',
'Mean reference paths':'Average number of released or reconstructed reference paths per evaluated instance or family.',
'Answer-Entity':'The released valid-answer entity set used as the primary target.',
'Graph-Answer-Entity':'Graph-consistent endpoints derived from released relation chains in the active graph. Not independently verified gold answers.',
'RW-Ans@p-actions::Answer-Entity':'Probability of reaching a released valid answer using uniformly sampled labeled actions at a fixed step horizon.',
'RW-Ans@p-neighbors::Answer-Entity':'Probability of reaching a released valid answer using uniformly sampled unique next-node neighbors.',
'RW-Ans@p-actions::Graph-Answer-Entity':'Probability of reaching a graph-consistent answer using uniformly sampled labeled actions.',
'RW-Ans@p-neighbors::Graph-Answer-Entity':'Probability of reaching a graph-consistent answer using uniformly sampled unique next-node neighbors.',
'Reachable-N-Count':'Number of distinct destinations reachable after exactly N random-walk actions.',
'Path-Count':'Number of distinct released annotated reference paths.',
'Graph-Path-Count':'Number of graph-relative paths consistent with the released relation chains.',
'Graph-Answer-Expansion':'Additional graph-consistent answer entities compared with released answer entities.',
'Graph-Path-Expansion':'Additional graph-relative paths compared with released annotated paths.',
'Clustering coefficient':'Fraction of possible neighbor-neighbor links that exist, averaged across entities in an undirected graph.',
'Eigenvector centrality':'Connectivity score that also gives higher weight to connections with other well-connected entities.',
'Out-degree':'Number of outgoing directed triples from an entity.',
'P95':'95th percentile: 95% of observations are at or below this value.',
'PED':'Path edit distance, measuring differences between predicted and reference navigation trajectories.',
'RED':'Relation edit distance, measuring differences between ordered relation sequences.',
'SELF_LOOP':'An optional action that leaves the walker at its current entity while consuming one step.'
};
function explanationFor(label){
const t=String(label??'').trim();
if(GLOSSARY[t])return GLOSSARY[t];
if(t.startsWith('RW-Ans@p-actions::'))return GLOSSARY['RW-Ans@p-actions'];
if(t.startsWith('RW-Ans@p-neighbors::'))return GLOSSARY['RW-Ans@p-neighbors'];
if(t==='family-macro'||t==='instance-micro')return GLOSSARY[t==='family-macro'?'Family-macro':'Instance-micro'];
return '';
}
function termHTML(label){const info=explanationFor(label);return info?`<span class="defined-term" tabindex="0" data-definition="${escaped(info)}">${escaped(label)}</span>`:escaped(label);}
const metric=(label,value,help='')=>`<div class="metric-card"><div class="metric-label">${termHTML(label)}</div><div class="metric-value">${escaped(value)}</div><div class="metric-help">${escaped(help)}</div></div>`;
function table(rows,cols){
return `<div class="table-box"><table><thead><tr>${cols.map(c=>`<th class="${c.right?'number':''}">${termHTML(c.label)}</th>`).join('')}</tr></thead><tbody>${rows.map(row=>'<tr>'+cols.map(c=>`<td class="${c.right?'number':''}">${termHTML(c.format?c.format(row[c.key]):row[c.key])}</td>`).join('')+'</tr>').join('')}</tbody></table></div>`;
}
function pairs(rows){return table(rows.map(([label,value])=>({label,value})),[{label:'Measure',key:'label'},{label:'Value',key:'value',right:true}]);}
function section(title,intro,content){return `<section class="report-section"><h3 class="section-title">${termHTML(title)}</h3>${intro?`<p class="section-intro">${intro}</p>`:''}${content}</section>`;}
function chart(title,items,key='count',nameKey='name'){
if(!items.length)return '<p class="muted">No data available.</p>';
const max=Math.max(1,...items.map(x=>x[key]||0));
return `<div class="plot-panel"><h3>${escaped(title)}</h3><div class="bars">${items.map(x=>{
const val=Number(x[key])||0, label=String(x[nameKey]); const width=Math.max(0,val/max*100);
return `<div class="bar-row" title="${escaped(label)} · ${number(val)}"><span class="bar-label">${escaped(label)}</span><div class="bar-track"><div class="bar-fill" style="width:${width}%"></div></div><span class="bar-value">${number(val)}</span></div>`;
}).join('')}</div></div>`;
}
function hintPanel(title,text){return `<div class="analysis-note"><strong>${escaped(title)}:</strong> ${escaped(text)}</div>`;}
function aggregateMetric(metricName, aggregation){
const table=(latest?.joint?.metric_tables||[]).find(x=>x.metric===metricName);
return table?.rows?.find(r=>r.subset==='All'&&r.aggregation===aggregation)||null;
}
function updateDashboardControls(){
const q=latest?.qa, j=latest?.joint;
const hasFamily=Boolean(q?.dashboard?.has_family_ids) && (
!j || !j.summary.random_walk_enabled || Boolean(aggregateMetric('RW-Ans@p-actions::Answer-Entity','family-macro')));
const hasGraphAnswer=Boolean(q?.summary?.rows_with_graph_answers>0) && (
!j || !j.summary.random_walk_enabled || Boolean(aggregateMetric('RW-Ans@p-actions::Graph-Answer-Entity','instance-micro')));
if(!hasFamily)dashboardAggregation='instance-micro';
if(!hasGraphAnswer)dashboardTarget='Answer-Entity';
$('dashboard-modes').classList.toggle('hidden',!hasFamily&&!hasGraphAnswer);
$('aggregation-mode-wrap').classList.toggle('hidden',!hasFamily);
$('answer-mode-wrap').classList.toggle('hidden',!hasGraphAnswer);
$('dashboard-aggregation').value=dashboardAggregation;
$('dashboard-answer').value=dashboardTarget;
$('dashboard-mode-caption').textContent=hasFamily||hasGraphAnswer?'Top summary cards only. Detailed report tables and exported statistics retain both views.':'';
}
function renderTopMetrics(){
if(!latest)return;
const cards=[];
if(latest.kg){const s=latest.kg.summary;
cards.push(metric('Unique triples',number(s.unique_triples),`${number(s.duplicate_triplet_rows)} duplicates found`),
metric('Entities',number(s.entities),'Active graph'),metric('Relations',number(s.relations),'Unique relation types'),
metric('Largest WCC',pct(s.largest_wcc_fraction),`${number(s.largest_wcc_nodes)} entities`));}
if(latest.qa){
const s=latest.qa.summary, m=latest.qa.dashboard?.family_macro||{}, macro=dashboardAggregation==='family-macro';
const targetGraph=dashboardTarget==='Graph-Answer-Entity';
const answerMean=targetGraph?(macro?m.graph_answer_count_mean:s.graph_answer_count_mean):(macro?m.answer_count_mean:s.answer_count_mean);
const wordMean=macro?m.paraphrase_count_mean:s.paraphrase_count_mean;
cards.push(metric('QA rows',number(s.qa_rows),'Selected benchmark rows'),
metric('Question families',number(s.families),'Structural family identifiers'),
metric('Mean answer count',answerMean===null||answerMean===undefined?'—':fixed(answerMean,3),
`${targetGraph?'Graph-consistent':'Released'} answers · ${dashboardAggregation}`),
metric('Mean paraphrases',wordMean===null||wordMean===undefined?'—':fixed(wordMean,3),dashboardAggregation));
}
if(latest.joint){
const s=latest.joint.summary,target=dashboardTarget, aggregation=dashboardAggregation;
const a=aggregateMetric(`RW-Ans@p-actions::${target}`,aggregation),
n=aggregateMetric(`RW-Ans@p-neighbors::${target}`,aggregation),
reach=aggregateMetric('Reachable-N-Count',aggregation),
ref=aggregateMetric('Reference-Path-Count',aggregation);
const targetLabel=target==='Answer-Entity'?'Released':'Graph-consistent';
if(s.random_walk_enabled){
cards.push(metric('RW-Ans@p-actions',a?sci(a.mean):'—',`${targetLabel} · ${aggregation} · n=${number(a?.n)}`),
metric('RW-Ans@p-neighbors',n?sci(n.mean):'—',`${targetLabel} · ${aggregation} · n=${number(n?.n)}`),
metric('Reachable N count',reach?number(reach.mean):'—',`Mean endpoints · ${aggregation}`));
}
cards.push(metric('Mean reference paths',ref?fixed(ref.mean,3):'—',`${aggregation} · n=${number(ref?.n)}`));
}
$('metric-cards').innerHTML=cards.join('');
}
function render(){
if(!latest)return;
const parts=[];if(latest.kg)parts.push(latest.kg.source.name);if(latest.qa)parts.push(latest.qa.source.name);
$('report-title').textContent=parts.length?parts.join(' + '):'Statistics summary';
const subtitles=[];
const sourceCaption=(src,kind)=>src.mode==='archive'?`${src.name} · GCS JSON API (ZIP)`:
src.mode==='url'?`${src.repo} · Hugging Face`:`Local ${kind} · ${src.filename||''}`;
if(latest.kg)subtitles.push(sourceCaption(latest.kg.source,'graph'));
if(latest.qa)subtitles.push(sourceCaption(latest.qa.source,'QA'));
subtitles.push(`${fixed(latest.runtime_seconds,2)} s`);
$('report-subtitle').textContent=subtitles.join(' · ');
updateDashboardControls();renderTopMetrics();
renderOverview();renderNavigation();renderQA();renderJoint();renderDistributions();renderRelations();
decorateStaticDefinitions();
}
function decorateStaticDefinitions(){
document.querySelectorAll('.check-row strong, .mode-label, .tab, .plot-panel h3').forEach(el=>{
const info=explanationFor(el.textContent.trim());
if(info){
el.removeAttribute('title');
el.dataset.definition=info;
el.classList.add('defined-term');
if(!el.matches('button, a, input, select'))el.tabIndex=0;
}
});
const aggregationLabel=document.querySelector('label[for="dashboard-aggregation"]');
const answerLabel=document.querySelector('label[for="dashboard-answer"]');
const explanations=[
[aggregationLabel,GLOSSARY['Instance-micro']+' '+GLOSSARY['Family-macro']],
[answerLabel,GLOSSARY['Answer (released)']+' '+GLOSSARY['Graph-Answer (graph-consistent)']]
];
for(const [label,info] of explanations){
if(label){label.removeAttribute('title');label.dataset.definition=info;label.classList.add('defined-term');label.tabIndex=0;}
}
$('dashboard-aggregation').removeAttribute('title');
$('dashboard-answer').removeAttribute('title');
}
function renderOverview(){
if(!latest.kg){$('tab-overview').innerHTML=hintPanel('KG overview unavailable','No graph source was selected for this run.'); return;}
const r=latest.kg,s=r.summary;
const metadataNote=r.source.entity_filename ?
`<div class="analysis-note"><strong>Entity metadata:</strong> ${escaped(r.source.entity_filename)} is ${r.source.include_metadata_only_entities?'included as an explicit graph entity list. Metadata-only IDs count as isolated nodes.':'used for display labels only. Graph entities are defined by the triplets.'}</div>` : '';
const issues=[]; if(r.parse.ignored_malformed_rows) issues.push(`${number(r.parse.ignored_malformed_rows)} malformed input rows were ignored.`); if(r.parse.bad_rows) issues.push(`${number(r.parse.bad_rows)} empty-field rows were ignored.`);
const warnings=Object.entries(r.optional).filter(([k,v])=>typeof v==='string').map(([k,v])=>`<div class="analysis-note warning"><strong>${escaped(k.replaceAll('_',' '))}:</strong> ${escaped(v)}</div>`).join('');
const optional=[]; if(s.clustering_coefficient!==undefined) optional.push(['Average clustering coefficient',sci(s.clustering_coefficient)]); if(s.legacy_max_non_cyclic_path!==undefined) optional.push(['Legacy max non-cyclic path',number(s.legacy_max_non_cyclic_path)]); if(s.legacy_krackhardt_mean!==undefined) optional.push(['Legacy Krackhardt mean',fixed(s.legacy_krackhardt_mean,5)]); if(r.optional.eigenvector_top) optional.push(['Eigenvector centrality','Computed. See Relations & entities.']);
$('tab-overview').innerHTML=metadataNote+`<div class="analysis-note">Navigation metrics use distinct <strong>(head, relation, tail)</strong> triples. Density and components collapse relation labels and edge directions.</div>`+
(issues.length?`<div class="analysis-note warning">${issues.map(escaped).join(' ')}</div>`:'')+warnings+
section('Graph overview','Counts and connectivity for the active graph.',pairs([
['Triplet rows',number(s.triplet_rows)],['Unique directed triples',number(s.unique_triples)],['Duplicate rows',number(s.duplicate_triplet_rows)],['Unique self-loop triples',number(s.self_loop_triples)],['Entities',number(s.entities)],['Relation types',number(s.relations)],['Isolated nodes',number(s.isolated_nodes)],['Zero-out-degree nodes',number(s.zero_out_degree_nodes)],['Weakly connected components',number(s.wcc_count)],['Largest WCC (nodes)',number(s.largest_wcc_nodes)],['Largest WCC fraction',pct(s.largest_wcc_fraction)],['Median WCC size',number(s.median_wcc_nodes)],['Singleton components',number(s.singleton_wcc_count)],['Graph density (undirected)',sci(s.graph_density)],['Legacy raw out-degree mean',fixed(s.out_degree_mean_legacy,4)],['Legacy raw in-degree mean',fixed(s.in_degree_mean_legacy,4)]
]))+
(optional.length?section('Optional metrics','Requested calculations that completed within the browser safeguards.',pairs(optional)):'')+
section('Largest components','Number of entities in each weakly connected component (top 12).',chart('Component size',r.components.map((count,i)=>({name:`WCC ${i+1}`,count})).slice(0,12)));
}
function renderNavigation(){
if(!latest.kg){$('tab-navigation').innerHTML=hintPanel('KG navigation unavailable','No graph source was selected for this run.'); return;}
const s=latest.kg.summary;
const groups=[{name:'Out-degree (unique triples)',prefix:'out_degree'},{name:'In-degree (unique triples)',prefix:'in_degree'},{name:'Unique outgoing neighbors',prefix:'unique_out_neighbors'},{name:'Distinct outgoing relations',prefix:'distinct_out_relations'}];
const rows=groups.map(g=>({measure:g.name,mean:fixed(s[g.prefix+'_mean'],4),median:number(s[g.prefix+'_median']),p90:number(s[g.prefix+'_p90']),p95:number(s[g.prefix+'_p95']),p99:number(s[g.prefix+'_p99']),maximum:number(s[g.prefix+'_max'])}));
$('tab-navigation').innerHTML=section('Navigation-oriented structural calibration','All percentiles include tail-only nodes and explicitly listed isolated nodes, whose outgoing values are zero.',
table(rows,[{label:'Metric',key:'measure'},{label:'Mean',key:'mean',right:true},{label:'Median',key:'median',right:true},{label:'P90',key:'p90',right:true},{label:'P95',key:'p95',right:true},{label:'P99',key:'p99',right:true},{label:'Max',key:'maximum',right:true}]))+
section('Branching thresholds','Fraction of all entities with unique-triple out-degree exceeding each threshold.',pairs([
['Out-degree > 10',pct(s.nodes_out_degree_gt_10_fraction)],['Out-degree > 50',pct(s.nodes_out_degree_gt_50_fraction)],['Out-degree > 100',pct(s.nodes_out_degree_gt_100_fraction)],['Zero out-degree entities',`${number(s.zero_out_degree_nodes)} / ${number(s.entities)}`]
]))+
`<div class="analysis-note">Out-degree counts distinct directed <strong>triples</strong>. If a single head has several relations pointing to the same tail, its out-degree can be larger than its unique outgoing-neighbor count.</div>`;
}
function renderQA(){
if(!latest.qa){$('tab-qa').innerHTML=hintPanel('QA analysis unavailable','No QA source was selected for this run.'); return;}
const q=latest.qa;
const summary=q.summary;
const hopItems=Object.entries(q.hops||{}).sort((a,b)=>Number(a[0])-Number(b[0])).map(([name,count])=>({name:`${name} hop`,count}));
const splitItems=Object.entries(q.splits||{}).map(([name,count])=>({name,count}));
const wordingItems=Object.entries(q.wording_modes||{}).map(([name,count])=>({name,count}));
const notes=[]; if(q.parse?.ignored_malformed_rows) notes.push(`${number(q.parse.ignored_malformed_rows)} malformed CSV rows were ignored.`);
$('tab-qa').innerHTML=(notes.length?`<div class="analysis-note warning">${notes.map(escaped).join(' ')}</div>`:'')+
section('QA summary','Row-level statistics for the selected QA collection after applying the split filter. PQ/PQL released and graph-consistent annotations are reported separately.',pairs([
['QA rows',number(summary.qa_rows)],['Question families',number(summary.families)],['Available splits',number(Object.keys(q.splits||{}).length)],['Maximum hop count',number(summary.max_hops)],['Mean answer count',fixed(summary.answer_count_mean,3)],['Median answer count',fixed(summary.answer_count_median,3)],['Mean paraphrase count',fixed(summary.paraphrase_count_mean,3)],['Rows with explicit path materialization',number(summary.rows_with_materialized_paths)],['Rows with relation-chain keys',number(summary.rows_with_path_key)],['Rows with split labels',number(summary.rows_with_split_label)]
]))+
section('Answer and wording composition','How many answers and paraphrases each QA row contains.',table([
{measure:'Answer count',mean:fixed(summary.answer_count_mean,3),median:fixed(summary.answer_count_median,3),p95:fixed(summary.answer_count_p95,3),max:number(summary.answer_count_max)},
{measure:'Paraphrase count',mean:fixed(summary.paraphrase_count_mean,3),median:fixed(summary.paraphrase_count_median,3),p95:fixed(summary.paraphrase_count_p95,3),max:number(summary.paraphrase_count_max)},
{measure:'Released reference path count',mean:fixed(summary.reference_path_count_mean,3),median:fixed(summary.reference_path_count_median,3),p95:fixed(summary.reference_path_count_p95,3),max:number(summary.reference_path_count_max)},
...(summary.graph_answer_count_mean!==null?[{measure:'Graph-consistent answer count',mean:fixed(summary.graph_answer_count_mean,3),median:fixed(summary.graph_answer_count_median,3),p95:fixed(summary.graph_answer_count_p95,3),max:number(summary.graph_answer_count_max)}]:[]),
...(summary.graph_path_count_mean!==null?[{measure:'Graph-relative path count',mean:fixed(summary.graph_path_count_mean,3),median:fixed(summary.graph_path_count_median,3),p95:fixed(summary.graph_path_count_p95,3),max:number(summary.graph_path_count_max)}]:[])
],[{label:'Measure',key:'measure'},{label:'Mean',key:'mean',right:true},{label:'Median',key:'median',right:true},{label:'P95',key:'p95',right:true},{label:'Max',key:'max',right:true}]))+
'<div class="two-charts">'+chart('Hop distribution',hopItems)+chart('Benchmark split distribution',splitItems)+'</div>'+
'<div class="two-charts">'+chart('Question wording availability',wordingItems)+chart('Answer multiplicity (most common)',(q.answer_distribution||[]).slice(0,12).map(x=>({name:String(x.value),count:x.count})))+'</div>'+
section('Released and graph-relative annotations','Released reference paths are obtained from Multi-Paths or Paths. Graph-Multi-Paths and Graph-Answer-Entity come from the uploaded QA CSV and are not recomputed for the selected KG. They do not redefine released gold answers.',pairs([
['Rows with released relation-chain keys',number(summary.rows_with_path_key)],
['Mean relation chains per row',fixed(summary.relation_chain_count_mean,3)],
['Maximum relation chains per row',number(summary.relation_chain_count_max)],
['Rows with released materialized paths',number(summary.rows_with_materialized_paths)],
['Rows with released Path-Count',number(summary.rows_with_path_count_field)],
['Rows whose topic is a released answer',number(summary.topic_is_answer_rows)],
['Rows with graph-consistent answers',number(summary.rows_with_graph_answers)],
['Rows whose topic is a graph-consistent answer',number(summary.topic_is_graph_answer_rows)],
['Rows with Graph-Multi-Paths',number(summary.rows_with_graph_paths)],
['Rows with Graph-Path-Count',number(summary.rows_with_graph_path_count_field)],
['Rows with cycles in released materialized paths',number(summary.rows_with_cyclic_materialized_paths)],
['Rows with immediate backtracking in released materialized paths',number(summary.rows_with_backtrack_materialized_paths)]
]));
}
function metricTableRows(metricTables, metricName){
const found=(metricTables||[]).find(x=>x.metric===metricName);
return found?.rows||[];
}
function renderJoint(){
if(!latest.joint){$('tab-joint').innerHTML=hintPanel('Joint KGQA analysis unavailable','Joint analysis requires both a graph and a QA collection.');return;}
const j=latest.joint,s=j.summary,withRW=s.random_walk_enabled!==false;
const entries=[
['Evaluated QA instances',number(s.qa_rows_evaluated)],['Released QA rows',number(s.canonical_qa_rows)],
['Paraphrase expansion',s.paraphrases_expanded?'Enabled':'Disabled'],
['Evaluated families',number(s.families_evaluated)],['Skipped rows (missing source entity)',number(s.skipped_missing_source)],
['Missing released/reconstructed reference paths',number(s.missing_reference_paths)],
['Rows with released/reconstructed reference paths',number(s.reference_paths_rows)],
['Random-walk probabilities',withRW?'Calculated':'Skipped (disabled)']
];
if(withRW)entries.push(
['Action space',s.action_space==='undirected'?'Undirected (reverse actions)':'Directed (outgoing only)'],
['Self-loop actions enabled',s.use_self_loops?'Yes':'No'],
['Random-walk horizon',number(s.num_rollout_steps)],
['Mean reachable endpoints after N steps',fixed(s.reachable_n_mean,3)],
['Mean RW-Ans@p-actions',sci(s.rw_actions_all_mean)],
['Mean RW-Ans@p-neighbors',sci(s.rw_neighbors_all_mean)]
);
const metricRows=name=>(j.metric_tables||[]).find(t=>t.metric===name)?.rows||[];
const summaryRows=(names,formatter=sci)=>names.flatMap(name=>metricRows(name)).map(r=>({metric:r.metric||'',subset:r.subset,aggregation:r.aggregation,n:r.n,mean:formatter(r.mean),median:formatter(r.median),std:formatter(r.std)}));
const summaryCols=[{label:'Metric',key:'metric'},{label:'Subset',key:'subset'},{label:'Aggregation',key:'aggregation'},{label:'N',key:'n',right:true},{label:'Mean',key:'mean',right:true},{label:'Median',key:'median',right:true},{label:'Std',key:'std',right:true}];
let out=section('Joint KGQA summary',withRW?'The active QA rows include exact fixed-horizon random-walk probabilities and reference structure.':'Random-walk probabilities were disabled. Reference-path and answer statistics are still calculated.',pairs(entries));
if(withRW){
out+=section('Random-walk answer calibration','Instance-micro and family-macro summaries follow the selected QA rows and their hop subsets.',
table(summaryRows(['RW-Ans@p-actions::Answer-Entity','RW-Ans@p-neighbors::Answer-Entity']),summaryCols));
const graphRows=summaryRows(['RW-Ans@p-actions::Graph-Answer-Entity','RW-Ans@p-neighbors::Graph-Answer-Entity']);
if(graphRows.length)out+=section('Secondary graph-consistent answer calibration','These probabilities target Graph-Answer-Entity. The QA CSV supplies the targets.',table(graphRows,summaryCols));
}
const expansion=summaryRows(['Graph-Answer-Expansion','Graph-Path-Expansion'],x=>fixed(x,4));
if(expansion.length)out+=section('Graph-relative expansion','Compares graph-answer and graph-path counts with released counts.',table(expansion,summaryCols));
const structural=summaryRows(['Answer-Count::Answer-Entity','Answer-Count::Graph-Answer-Entity','Released-Answer-Count','Graph-Answer-Count','Topic-Is-Released-Answer','Topic-Is-Graph-Answer','Path-Count','Graph-Path-Count','Reference-Path-Count','Released-Reference-Has-Cycle','Released-Reference-Has-Immediate-Backtrack'],x=>fixed(x,4));
if(!withRW&&structural.length)out+=section('Reference and answer structure','These metrics do not require a random-walk simulation.',table(structural,summaryCols));
if(withRW){
const reach=metricRows('Reachable-N-Count');
out+=section('Reachability and reference structure','Reachability depends on the source entity and fixed horizon. Reference metrics use released or reconstructable paths.',
table(reach.map(r=>({subset:r.subset,aggregation:r.aggregation,n:r.n,mean:fixed(r.mean,3),median:fixed(r.median,3),std:fixed(r.std,3)})),
[{label:'Subset',key:'subset'},{label:'Aggregation',key:'aggregation'},{label:'N',key:'n',right:true},{label:'Mean reachable endpoints',key:'mean',right:true},{label:'Median',key:'median',right:true},{label:'Std',key:'std',right:true}]));
const refProb=metricRows('RW-RefPath@p-actions');
if(refProb.length)out+=section('Released reference-path probability','Probability of executing an annotated path and padding to the fixed horizon with SELF_LOOP actions.',
table(refProb.map(r=>({subset:r.subset,aggregation:r.aggregation,n:r.n,mean:sci(r.mean),median:sci(r.median),std:sci(r.std)})),
[{label:'Subset',key:'subset'},{label:'Aggregation',key:'aggregation'},{label:'N',key:'n',right:true},{label:'Mean',key:'mean',right:true},{label:'Median',key:'median',right:true},{label:'Std',key:'std',right:true}]));
}
if(j.reference_state_actions){const ref=j.reference_state_actions;
out+=section('Reference-state action space','Distribution of actions at unique (hop, entity) states along reference paths.',pairs([
['States counted',number(ref.n)],['Mean actions',fixed(ref.mean,3)],['Median actions',fixed(ref.median,3)],
['P95 actions',fixed(ref.p95,3)],['Max actions',number(ref.max)],
['Fraction with > 10 actions',pct(ref.fraction_gt10)],['Fraction with > 50 actions',pct(ref.fraction_gt50)],['Fraction with > 100 actions',pct(ref.fraction_gt100)]
]));
}
out+=withRW?'<div class="analysis-note">RW-Ans@p-actions samples labeled actions. RW-Ans@p-neighbors samples unique neighbors. Dead ends remain absorbing.</div>':'<div class="analysis-note">Random-walk evaluation was skipped as requested. Enable the option and rerun to calculate RW-Ans, RW-RefPath, and fixed-horizon reachability.</div>';
$('tab-joint').innerHTML=out;
}
function renderDistributions(){
if(!latest.kg){$('tab-distribution').innerHTML=hintPanel('Distribution view unavailable','No graph source was selected for this run.'); return;}
const d=latest.kg.distributions, names={out_degree:'Out-degree',in_degree:'In-degree',unique_out_neighbors:'Unique outgoing neighbors',distinct_out_relations:'Distinct outgoing relations'};
$('tab-distribution').innerHTML='<p class="section-intro">Entity-frequency distributions for navigation metrics, including zero-valued nodes. The graphs display the 18 most common values.</p><div class="two-charts">'+
Object.entries(names).map(([key,name])=>{
const dist=[...(d[key]||[])].sort((a,b)=>b.count-a.count||a.degree-b.degree).slice(0,18).map(x=>({name:String(x.degree),count:x.count}));
return chart(name,dist);
}).join('')+'</div>';
}
function renderRelations(){
if(!latest.kg){$('tab-relations').innerHTML=hintPanel('Relation and entity view unavailable','No graph source was selected for this run.'); return;}
const r=latest.kg, ren=x=>labels.get(x)||x, en=x=>entityLabels.has(x)?`${entityLabels.get(x)} (${x})`:x;
const top=(r.relations||[]).slice(0,20).map(x=>({...x,pretty:ren(x.name)}));
const relTable=table(top.map(x=>({relation:x.pretty+(x.pretty!==x.name?` (${x.name})`:''),rows:x.count,unique:x.unique_triples})),[{label:'Relation',key:'relation'},{label:'Rows',key:'rows',right:true},{label:'Unique triples',key:'unique',right:true}]);
let extra=''; if(r.optional.eigenvector_top) extra=section('Top eigenvector centrality','Undirected projection, optional iterative computation.',table(r.optional.eigenvector_top.map(v=>({...v,name:en(v.name),score:fixed(v.value,6)})),[{label:'Entity',key:'name'},{label:'Score',key:'score',right:true}]));
if(r.optional.legacy_path_per_relation) extra+=section('Legacy path and hierarchy by relation','Per-relation maximum cycle-pruned path length and Krackhardt reachability score.',table([...r.optional.legacy_path_per_relation].sort((a,b)=>b.maximum-a.maximum).slice(0,30).map(v=>({name:ren(v.relation),maximum:v.maximum,hierarchy:v.krackhardt===null?'Skipped':fixed(v.krackhardt,4)})),[{label:'Relation',key:'name'},{label:'Max path',key:'maximum',right:true},{label:'Hierarchy',key:'hierarchy',right:true}]));
$('tab-relations').innerHTML=section('Most frequent relations','Raw input-row frequency and unique-triple frequency.',chart('Top relation frequencies',top.slice(0,12).map(x=>({name:x.pretty,count:x.count})))+relTable)+
'<div class="two-charts">'+chart('Most frequent entities',(r.topNodes||[]).slice(0,12).map(x=>({name:en(x.name),count:x.total_degree_rows})))+chart('Highest incident relation diversity',(r.diverseNodes||[]).slice(0,12).map(x=>({name:en(x.name),count:x.distinct_incident_relations})))+'</div>'+
section('Entities by frequency','Number of input triple rows involving each entity as head or tail.',table((r.topNodes||[]).slice(0,20).map(v=>({name:en(v.name),head:v.out_degree_rows,tail:v.in_degree_rows,total:v.total_degree_rows})),[{label:'Entity',key:'name'},{label:'Head',key:'head',right:true},{label:'Tail',key:'tail',right:true},{label:'Total',key:'total',right:true}]))+extra;
}
function switchTab(target){document.querySelectorAll('[role=tab]').forEach(el=>{const on=el.dataset.tab===target;el.classList.toggle('active',on);el.setAttribute('aria-selected',String(on));}); document.querySelectorAll('.tab-pane').forEach(el=>el.classList.toggle('hidden',el.id!==`tab-${target}`));}
function saveBlob(data,name,type){const blob=new Blob([data],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),2500);}
function basename(){return 'kgqa_statistics';}
function exportJSON(){if(!latest)return; const data={...latest,metadata:{tool:'THESEUS KGQA Statistics Lab',created_utc:new Date().toISOString()}}; saveBlob(JSON.stringify(data,null,2),`${basename()}_report.json`,'application/json');}
function csvCell(x){const s=String(x??''); return /[,\n"\r]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;}
function exportCSV(){if(!latest?.kg){reportError('Node CSV export is only available when a graph source is selected.'); return;} const fields=['name','out_degree','in_degree','out_degree_rows','in_degree_rows','unique_out_neighbors','distinct_out_relations','distinct_incident_relations','total_degree_rows']; const lines=[['name','title',...fields.slice(1)].join(','),...(latest.kg.nodes||[]).map(n=>[csvCell(n.name),csvCell(entityLabels.get(n.name)||''),...fields.slice(1).map(f=>csvCell(n[f]))].join(','))]; saveBlob(lines.join('\n'),`${basename()}_node_statistics.csv`,'text/csv;charset=utf-8');}
document.querySelectorAll('input[name=graph-source], input[name=qa-source]').forEach(el=>el.addEventListener('change',updateSourceHints));
$('dataset-provider').addEventListener('change',updateSourceHints);
$('relations-file').addEventListener('change',()=>onColumnFileChange('relation','relations-file'));
$('entities-file').addEventListener('change',()=>onEntityFileChange());
for(const id of ['relation-id-column','relation-label-column','node-id-column','node-label-column'])
$(id).addEventListener('change',()=>{
if(latest&&!running){
loadLabels().catch(()=>{});
if(id==='node-id-column' && $('opt-include-metadata-entities').checked)status('Entity ID column changed. Rerun the analyses to update explicitly included metadata-only nodes.');
}
});
$('analyze').addEventListener('click',run); $('cancel').addEventListener('click',cancel);
$('clear-download-cache').addEventListener('click',clearDatasetCache);
$('opt-run-random-walk').addEventListener('change',updateRandomWalkControls); $('json-export').addEventListener('click',exportJSON); $('csv-export').addEventListener('click',exportCSV); document.querySelectorAll('[role=tab]').forEach(el=>el.addEventListener('click',()=>switchTab(el.dataset.tab)));
$('dashboard-aggregation').addEventListener('change',event=>{dashboardAggregation=event.target.value;renderTopMetrics();});
$('dashboard-answer').addEventListener('change',event=>{dashboardTarget=event.target.value;renderTopMetrics();});
updateSourceHints();
updateDatasetCacheStatus();
updateRandomWalkControls();