Download app.js from HalcyonSolutions/KGQA-Statistics-Lab: direct link, hf CLI and curl.
- Browser
- Download file 56.5 kB
-
https://huggingface.co/spaces/HalcyonSolutions/KGQA-Statistics-Lab/resolve/main/app.js
- Command line
-
hf download hf://spaces/HalcyonSolutions/KGQA-Statistics-Lab/app.js
-
curl -L -o app.js https://huggingface.co/spaces/HalcyonSolutions/KGQA-Statistics-Lab/resolve/main/app.js
56.5 kB
| ; | |
| // 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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(); | |