Hugdrigo's picture
Upload 3 files
7555bc7 verified
Raw History Blame Contribute Delete
55.1 kB
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0a1220"><meta name="description" content="CiteGraph: privacy-first scholarly citation network analysis with transparent and reproducible metrics."><title>CiteGraph · Scholarly Network Intelligence</title>
<style>:root{color-scheme:dark;--bg:#080f1c;--side:#0b1423;--panel:#101b2b;--panel2:#152236;--panel3:#1b2a40;--line:#273750;--muted:#8b9bb1;--text:#edf4fc;--accent:#54d7b3;--accent2:#98a7ff;--warn:#f5b46b;--red:#ff8c9a;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-size:14px}button,select,input{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}h1,h2,h3,p{margin-top:0}h1{letter-spacing:-1.6px}a{color:var(--accent)}.app-shell{min-height:100vh;display:grid;grid-template-columns:252px minmax(0,1fr)}.sidebar{background:var(--side);padding:29px 17px 20px;border-right:1px solid #1c2b40;display:flex;flex-direction:column;position:sticky;top:0;height:100vh;z-index:10}.brand{display:flex;align-items:center;gap:11px;padding:1px 12px 40px}.brand-symbol{width:40px;height:40px;border:1px solid #285f5e;background:#113237;color:#83edc9;display:flex;align-items:center;justify-content:center;border-radius:13px;font-size:30px}.brand b{display:block;font-size:21px;letter-spacing:-.7px}.brand b span{color:#7de9c4}.brand small{font-size:8px;letter-spacing:1.6px;color:#8b9fb4;font-weight:800}.sidebar-head{font-size:10px;color:#6c8198;letter-spacing:1.9px;font-weight:800;display:flex;justify-content:space-between;padding:7px 14px 17px}.sidebar-head span{color:#47647d}.sidebar-nav{display:grid;gap:6px}.nav-link{display:flex;align-items:center;gap:12px;color:#9baac1;background:transparent;text-align:left;border:1px solid transparent;border-radius:9px;padding:13px 12px;font-size:12.5px;font-weight:650;width:100%}.nav-link:hover,.nav-link:focus-visible{background:#152339;color:#edf6ff}.nav-link.active{background:#12382f;color:#a7f9d8;border-color:#206552}.nav-icon{font-size:20px;font-weight:500;width:22px;text-align:center}.nav-num{margin-left:auto;color:#64778b;font-size:10px}.nav-link.active .nav-num{color:#73d9b3}.sidebar-spacer{flex:1}.sidebar-bottom{margin:0 8px;padding:17px 10px 0;border-top:1px solid #29384c}.privacy-marker{font-size:11px;font-weight:800;letter-spacing:1px;color:#adf3d8}.dot,.secure-dot{color:#56d4a6}.dot{background:#5ad3a8;display:inline-block;height:7px;width:7px;border-radius:50%;margin-right:6px}.sidebar-bottom p{color:#8999ad;font-size:11px;line-height:1.65;margin:11px 0 20px}.sidebar-version{font-size:10px;color:#6e8094;display:flex;justify-content:space-between}.sidebar-version span{color:#98a6bb}.main-shell{min-width:0}.topbar{height:79px;border-bottom:1px solid #243249;display:flex;justify-content:space-between;align-items:center;padding:0 40px;background:#0a1321}.breadcrumb{font-size:11px;letter-spacing:1.3px;font-weight:800;color:#8294aa}.breadcrumb span{margin:0 12px;color:#425771}.breadcrumb strong{color:#dae8f6}.top-actions{display:flex;gap:20px;align-items:center}.secure-dot{font-size:10px;letter-spacing:1.1px;font-weight:800}.btn{padding:10px 15px;border-radius:8px;border:1px solid transparent;font-size:12px;font-weight:800;transition:all .12s;white-space:nowrap}.btn-primary{background:#5fd7b6;color:#071c1a}.btn-primary:hover{background:#a1f9dd}.btn-outline{background:#18273a;border-color:#33465f;color:#d4e4f2}.btn-outline:hover{border-color:#6fc9ba;background:#20384b}.btn-soft{background:#1c3537;border-color:#315d5a;color:#94e3cc}.btn[disabled]{opacity:.4;cursor:not-allowed}.mobile-row{display:none}.hamburger{border:1px solid var(--line);background:var(--panel);border-radius:8px;color:var(--text);padding:8px 11px}main{padding:38px 40px 25px;max-width:1650px;margin:auto}.page-heading{display:flex;justify-content:space-between;align-items:flex-end;gap:25px;padding-bottom:29px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--accent);font-weight:900;margin-bottom:12px}.page-heading h1{font-size:clamp(27px,3.15vw,44px);line-height:1.12;margin-bottom:13px;font-weight:770}.page-heading em{font-weight:500;color:#9df0d3;font-style:normal}.page-heading p{max-width:710px;line-height:1.55;color:#94a5b9;font-size:13px;margin:0}.heading-tools{width:280px;flex:none}.field-label{color:#8498ac;font-size:10px;font-weight:850;letter-spacing:1.3px;display:block;margin-bottom:10px}.select,.field-input{background:#162439;border:1px solid #2d405a;color:#e3edf8;border-radius:9px;padding:12px;font-size:12px;width:100%;max-width:100%;outline:none}.select{cursor:pointer}.field-input:focus,.select:focus{border-color:#6ecab5}.dataset-caption{display:block;margin-top:8px;font-size:9px;letter-spacing:1px;color:#7e94a8}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:18px}.kpi{background:var(--panel);border:1px solid #26374e;border-radius:12px;padding:20px;position:relative;min-height:130px}.kpi .label{font-size:10px;letter-spacing:1.15px;color:#8b9db1;font-weight:850}.kpi strong{font-size:31px;line-height:1.25;display:block;margin:11px 0 9px;letter-spacing:-1.3px}.kpi .context{font-size:11px;color:#8496ab}.kpi .mark{position:absolute;right:20px;top:19px;font-size:21px;color:#77c9b5}.grid-2{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(285px,.9fr);gap:16px;margin-bottom:16px}.grid-half{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-bottom:16px}.panel{background:var(--panel);border:1px solid #293a51;border-radius:12px;min-width:0;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 21px 15px;border-bottom:1px solid #22344a}.panel-head h2{font-size:14px;font-weight:770;margin:0;letter-spacing:-.15px}.panel-head p{color:#8298ad;font-size:10.5px;margin:5px 0 0}.panel-body{padding:19px 21px}.panel-caption{font-size:11px;line-height:1.65;color:#8ea0b2;margin:8px 0 0}.pill{font-size:9px;letter-spacing:.8px;background:#1d3441;color:#98f4d8;border:1px solid #2a5b53;padding:6px 9px;border-radius:30px;font-weight:850;white-space:nowrap}.pill.muted{background:#1f2c40;border-color:#314158;color:#a6b7cc}.graph-stage{width:100%;position:relative;background:radial-gradient(circle at center,#162b3a 0%,#101b2b 67%);min-height:345px;overflow:hidden}.graph-stage svg{display:block;width:100%;height:auto;max-height:560px}.graph-key{display:flex;gap:13px;flex-wrap:wrap;padding:10px 19px 16px;color:#91a5b9;font-size:10px}.graph-key b{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}.graph-buttons{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.graph-buttons .mini-select{max-width:170px}.mini-select{background:#142338;border:1px solid #31445c;border-radius:7px;padding:8px;color:#e6f4ff;font-size:11px}.graph-point{cursor:pointer}.graph-point circle{transition:r .12s,stroke-width .12s}.graph-point:hover circle{stroke:#fff;stroke-width:3}.graph-point text{font-size:11px;fill:#c6d5e2;paint-order:stroke;stroke:#101b2b;stroke-width:3;pointer-events:none}.graph-edge{stroke:#527d8e;opacity:.32;stroke-width:1.5;fill:none}.graph-edge.dim{opacity:.09}.graph-node.dim{opacity:.15}.tiny-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mini-stat{padding:14px;background:#17263a;border:1px solid #2a3d52;border-radius:9px}.mini-stat small{display:block;color:#94a5b7;font-size:10px}.mini-stat strong{font-size:19px;display:block;margin-top:7px}.bars{display:grid;gap:17px}.bar-row{min-width:0}.bar-info{display:flex;justify-content:space-between;gap:10px;font-size:11px;margin-bottom:7px}.bar-info b{font-weight:650;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.bar-info span{color:#9db0c3;flex:none}.bar-bg{height:7px;background:#243448;border-radius:8px;overflow:hidden}.bar-fill{height:100%;border-radius:8px}.divider{height:1px;background:#293950;margin:15px 0}.rank-list{display:grid;gap:0}.rank-row{display:grid;grid-template-columns:35px minmax(0,1fr) 70px 80px;align-items:center;gap:8px;padding:14px 5px;border-bottom:1px solid #24344b;cursor:pointer}.rank-row:hover{background:#18293f}.rank-row:last-child{border-bottom:0}.rank-number{color:var(--accent);font-size:12px;font-weight:900}.rank-title{font-weight:730;color:#eaf4fc;font-size:12px;line-height:1.4}.rank-sub{color:#8496ab;font-size:10px;margin-top:3px}.right{text-align:right}.rank-score{font-weight:850;color:#b5f7df}.timeline{width:100%;height:auto;display:block}.row{display:flex;align-items:center;gap:10px}.between{justify-content:space-between}.wrap{flex-wrap:wrap}.search-wrap{position:relative}.search-wrap input{padding-left:36px}.search-mark{position:absolute;left:12px;top:12px;color:#8aa0b4}.paper-card{background:#152338;border:1px solid #2e4258;border-radius:9px;padding:14px;cursor:pointer;text-align:left;color:inherit;width:100%}.paper-card:hover,.paper-card.selected{border-color:#5aa992;background:#1a353b}.paper-title{font-size:13px;font-weight:800;line-height:1.4;margin-bottom:7px}.paper-meta{font-size:11px;color:#8fa4ba;line-height:1.5}.paper-topic{display:inline-block;margin-top:8px;border:1px solid #35556a;color:#a2dbdc;border-radius:20px;padding:3px 9px;font-size:10px}.paper-list{display:grid;gap:8px;max-height:520px;overflow:auto;padding-right:3px}.details-title{font-size:19px;letter-spacing:-.45px;line-height:1.33;margin-bottom:13px}.details-meta{font-size:12px;color:#8da2b7;line-height:1.7}.details-links{max-height:220px;overflow:auto;display:grid;gap:7px}.link-item{border:1px solid #293a51;border-radius:7px;background:#172538;padding:9px;text-align:left;color:#bed2e2;font-size:11px;width:100%}.link-item:hover{border-color:#57c5aa}.section-label{font-size:10px;font-weight:900;letter-spacing:1.3px;color:#99b1c3;margin:14px 0 10px}.tag-row{display:flex;gap:7px;flex-wrap:wrap}.tag{padding:5px 9px;color:#b7c9d9;border-radius:18px;border:1px solid #37506a;background:#1a2c40;font-size:10px}.empty{text-align:center;color:#9aabbc;padding:30px 10px;font-size:12px}.notice{background:#1b302f;border:1px solid #34645a;border-radius:9px;padding:13px 15px;line-height:1.55;color:#bfeadb;font-size:12px}.notice.warning{background:#322b25;border-color:#665043;color:#f2cfac}.audit-list{display:grid;gap:9px}.audit-item{border:1px solid #2d4356;background:#172536;border-radius:9px;padding:12px 14px;line-height:1.6;color:#beccda;font-size:12px;display:flex;gap:10px}.audit-item span{color:var(--warn)}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.code-snippet{background:#0a1320;border:1px solid #2d4055;padding:15px;border-radius:9px;color:#9ee2cc;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.7}.file-box{padding:17px;border:1px dashed #4e657c;border-radius:10px;background:#132137;margin-bottom:14px}.file-box input{display:block;width:100%;font-size:12px;margin-top:10px;color:#a7bad0}.file-box strong{font-size:12px}.field-note{font-size:10.5px;line-height:1.6;color:#90a5ba;margin:8px 0 0}.action-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.action-grid .btn{text-align:left;white-space:normal}.section-title{font-size:15px;font-weight:850;margin-bottom:15px}.footer{color:#64798c;font-size:9px;letter-spacing:1px;font-weight:700;border-top:1px solid #253448;margin-top:32px;padding:24px 0;display:flex;justify-content:space-between;gap:15px;flex-wrap:wrap}.toast{position:fixed;bottom:22px;right:22px;max-width:min(460px,calc(100vw - 44px));padding:14px 20px;color:#dffcec;background:#174336;border:1px solid #45947b;border-radius:9px;box-shadow:0 12px 28px #02061099;display:none;font-size:13px;z-index:100}.toast.error{background:#42252c;border-color:#a45165;color:#ffe1e8}.toast.show{display:block}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;clip:rect(0,0,0,0);overflow:hidden;white-space:nowrap}.topic-card{padding:18px;border:1px solid #2b3d50;border-radius:10px;background:#17263a}.topic-card .top{font-size:13px;font-weight:800;margin-bottom:11px}.topic-card .big{font-size:25px;font-weight:850}.topic-card .muted{font-size:11px;color:#9bb1c2}.topic-card .stripe{height:4px;margin-top:14px;border-radius:5px}.model-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:13px;margin-bottom:18px}.two-details{display:grid;grid-template-columns:1.05fr .95fr;gap:15px}.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.toolbar .field-input{max-width:230px}.single-column{display:grid;gap:16px}.small-muted{font-size:11px;color:#94a8bb}.section-legend{color:#8ba0b3;font-size:11px;line-height:1.6}
@media(max-width:1120px){.app-shell{grid-template-columns:213px minmax(0,1fr)}.sidebar{padding-left:10px;padding-right:10px}.nav-link{padding:11px 8px;font-size:11px}.topbar{padding:0 25px}main{padding:30px 25px 22px}.heading-tools{width:245px}.grid-2{grid-template-columns:1.3fr .8fr}}
@media(max-width:830px){.app-shell{display:block}.sidebar{position:fixed;left:0;top:0;bottom:0;width:265px;transform:translateX(-101%);transition:transform .18s ease;box-shadow:12px 0 30px #02040a90}.sidebar.open{transform:translateX(0)}.topbar{height:65px;padding:0 19px}.breadcrumb{display:none}.mobile-row{display:flex;align-items:center;gap:10px}.mobile-brand{color:#a4f1d6;font-weight:850;font-size:16px}.top-actions{gap:10px}main{padding:26px 18px}.page-heading{align-items:flex-start;flex-direction:column;gap:19px}.heading-tools{width:100%;max-width:390px}.page-heading h1{font-size:32px}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-2,.grid-half,.two-details{grid-template-columns:1fr}.model-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:480px){.topbar{padding:0 13px}.top-actions .secure-dot{display:none}main{padding:24px 12px}.page-heading h1{font-size:29px}.kpi{padding:15px;min-height:120px}.kpi strong{font-size:26px}.panel-head{padding:15px}.panel-body{padding:14px}.rank-row{grid-template-columns:22px minmax(0,1fr) 50px 55px;gap:4px}.rank-score{font-size:11px}.rank-title{font-size:11px}.action-grid{grid-template-columns:1fr}.graph-key{padding:10px}.graph-stage{min-height:260px}.grid-2{gap:12px}.footer{line-height:1.7}.top-actions .btn{padding:9px 10px;font-size:11px}}
</style></head><body>
<div class="app-shell">
<aside class="sidebar" id="sidebar">
<div class="brand"><span class="brand-symbol">✧</span><div><b>Cite<span>Graph</span></b><small>RESEARCH INTELLIGENCE</small></div></div>
<div class="sidebar-head">WORKSPACE <span>01</span></div>
<nav class="sidebar-nav" id="nav" aria-label="Application navigation">
<button data-view="overview" class="nav-link active"><span class="nav-icon">◫</span> Overview <span class="nav-num">01</span></button>
<button data-view="network" class="nav-link"><span class="nav-icon">⠿</span> Network Explorer <span class="nav-num">02</span></button>
<button data-view="influence" class="nav-link"><span class="nav-icon">↗</span> Influence Rankings <span class="nav-num">03</span></button>
<button data-view="themes" class="nav-link"><span class="nav-icon">◈</span> Themes & Timeline <span class="nav-num">04</span></button>
<button data-view="audit" class="nav-link"><span class="nav-icon">▤</span> Audit & Methods <span class="nav-num">05</span></button>
<button data-view="data" class="nav-link"><span class="nav-icon">⇅</span> Data & Exports <span class="nav-num">06</span></button>
</nav>
<div class="sidebar-spacer"></div><div class="sidebar-bottom"><div class="privacy-marker"><span class="dot"></span> LOCAL ANALYSIS</div><p>Your data stays in this browser. No uploads, no external APIs.</p><div class="sidebar-version">CiteGraph / V1.0 <span>PROJECT 43</span></div></div>
</aside>
<div class="main-shell"><header class="topbar"><div class="mobile-row"><button class="hamburger" id="menuBtn" aria-label="Toggle navigation" aria-expanded="false">☰</button><span class="mobile-brand">✧ CiteGraph</span></div><div class="breadcrumb">RESEARCH LAB <span>/</span> <strong id="breadcrumb">OVERVIEW</strong></div><div class="top-actions"><span class="secure-dot">● OFFLINE</span><button class="btn btn-outline" id="downloadReport">↓ Export report</button></div></header>
<main id="main" tabindex="-1"><section class="page-heading"><div><div class="eyebrow" id="eyebrow">01 / RESEARCH OVERVIEW</div><h1 id="pageTitle">Mapping the structure<br><em>of scientific knowledge.</em></h1><p id="pageSubtitle">Explore research connections, trace citations and reveal intellectual influence with auditable metrics.</p></div><div class="heading-tools"><label class="field-label" for="presetSelect">DEMONSTRATION COLLECTION</label><select id="presetSelect" class="select"><option value="ai">Responsible AI Research</option><option value="climate">Climate Resilience Systems</option><option value="education">Cross-Border Education</option></select><span class="dataset-caption" id="datasetCaption">SYNTHETIC SAMPLE · REPRODUCIBLE</span></div></section>
<div class="content" id="content" aria-live="polite"></div>
<footer class="footer"><span>✧ CITEGRAPH · AUDITABLE SCHOLARLY ANALYTICS</span><span>All demo references are fictional · Internal-corpus metrics only</span></footer></main></div>
</div><div class="toast" role="status" id="toast" aria-live="polite"></div>
<script>/* CiteGraph analytical kernel. Pure, browser and Node compatible. MIT License. */
(function(root){
'use strict';
const C={};
const TEXTMAX=260;
C.safeText=(v,n=TEXTMAX)=>String(v??'').replace(/[\u0000-\u001F\u007F]/g,' ').trim().slice(0,n);
C.csvEscape=v=>{let s=String(v??'');if(/^[\s]*[=+@-]/.test(s))s="'"+s;return /[",\r\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};
C.csv=(rows)=>rows.map(r=>r.map(C.csvEscape).join(',')).join('\r\n')+'\r\n';
C.csvParse=(str)=>{
if(typeof str!=='string'||str.length>1_500_000)throw Error('CSV missing or larger than 1.5 MB');
str=str.replace(/^\uFEFF/,'');let rows=[],r=[],v='',quoted=false,postQuote=false;
for(let i=0;i<str.length;i++){
let c=str[i];if(quoted){if(c==='"'){if(str[i+1]==='"'){v+='"';i++;}else{quoted=false;postQuote=true;}}else{v+=c;}continue;}
if(postQuote){if(c===','){r.push(v);v='';postQuote=false;}else if(c==='\n'||c==='\r'){r.push(v);rows.push(r);r=[];v='';postQuote=false;if(c==='\r'&&str[i+1]==='\n')i++;}else if(c!==' '&&c!=='\t'){throw Error('Unexpected character after closing CSV quote');}continue;}
if(c==='"'){if(v.trim()!=='')throw Error('Quote inside unquoted CSV cell');quoted=true;}
else if(c===','){r.push(v);v='';}
else if(c==='\r'||c==='\n'){r.push(v);rows.push(r);r=[];v='';if(c==='\r'&&str[i+1]==='\n')i++;}
else v+=c;
}
if(quoted)throw Error('Unclosed CSV quote');if(postQuote||v!==''||r.length){r.push(v);rows.push(r);}
rows=rows.filter(row=>row.some(x=>x.trim()!==''));if(rows.length>550)throw Error('Maximum 550 CSV rows');if(rows.length<2)throw Error('CSV requires a header and at least one record');const width=rows[0].length;if(width>30)throw Error('Too many columns');if(rows.some(r=>r.length!==width))throw Error('Inconsistent column count');return rows;
};
C.toRecords=(text,headers)=>{const a=C.csvParse(text);const keys=a.shift().map(x=>x.trim().toLowerCase());if(keys.some((k,i)=>keys.indexOf(k)!==i))throw Error('Duplicate column names');const miss=headers.filter(h=>!keys.includes(h));if(miss.length)throw Error('Missing column(s): '+miss.join(', '));return a.map(row=>Object.fromEntries(keys.map((k,i)=>[k,row[i]])));};
C.validate=(papers,citations)=>{
if(!Array.isArray(papers)||!Array.isArray(citations))throw Error('Dataset requires papers and citations arrays');
if(papers.length<3||papers.length>120)throw Error('Between 3 and 120 papers required');
if(citations.length>600)throw Error('Too many citations (max 600)');
const ids=new Set();const p=papers.map((x,i)=>{
if(!x||typeof x!=='object')throw Error('Paper '+(i+1)+' is invalid');
const id=C.safeText(x.id,40);if(!/^[a-zA-Z0-9_.:-]{1,40}$/.test(id))throw Error('Invalid ID at paper '+(i+1));if(ids.has(id))throw Error('Duplicate paper ID: '+id);ids.add(id);
const year=Number(x.year);if(!Number.isInteger(year)||year<1950||year>2035)throw Error('Invalid year for '+id);
const title=C.safeText(x.title,160),topic=C.safeText(x.topic,35),authors=C.safeText(x.authors,120);
if(!title||!topic||!authors)throw Error('Paper '+id+' needs title, topic and authors');
return {id,year,title,topic,authors};
});
const pMap=new Map(p.map(x=>[x.id,x]));let seen=new Set();const e=citations.map((x,i)=>{
const source=C.safeText(x.source,40),target=C.safeText(x.target,40),key=source+'→'+target;
if(!pMap.has(source)||!pMap.has(target))throw Error('Citation '+(i+1)+' refers to unknown paper');
if(source===target)throw Error('Self-citations not supported');if(seen.has(key))throw Error('Duplicate citation: '+key);seen.add(key);
if(pMap.get(source).year<pMap.get(target).year)throw Error('Citation from older to newer paper: '+key);
return {source,target};
});
return {papers:p,citations:e};
};
C.prng=seed=>{let s=seed>>>0;return ()=>((s=Math.imul(1664525,s)+1013904223>>>0)/4294967296);};
const PRESETS=[
{key:'ai',name:'Responsible AI Research',subtitle:'Interpretability · robustness · model governance',seed:381,
topics:['Explainability','Evaluation','Robustness','Governance'],titles:[
'Interpretable predictions under uncertainty','Evaluating reasoning with multilingual benchmarks','Model audit protocols for deployment','Robust aggregation for collaborative training','Transparent decision frameworks','Faithful feature attribution methods','Trust calibration in adaptive systems','Monitoring language systems for drift','Evaluation of model stability across domains','Governance signals in model documentation','Human oversight in algorithmic triage','Counterfactual explanations with constraints','Reliable model comparison under shift','Auditing datasets for hidden overlap','Reproducible tests for decision systems','Group-aware uncertainty evaluation','Stress testing robustness claims','Longitudinal monitoring of deployed models','Interpretability beyond feature rankings','Responsible deployment assessment','Hybrid auditing of decision pipelines','Verification of model evidence trails']},
{key:'climate',name:'Climate Resilience Systems',subtitle:'Adaptation · infrastructure · risk · policy',seed:726,
topics:['Climate Risk','Infrastructure','Adaptation','Policy'],titles:[
'Mapping compound flood risks in cities','Infrastructure fragility under heat stress','Community adaptation pathway analysis','Evaluating policies for water resilience','Network reliability during extreme weather','Regional drought exposure indices','Uncertainty in urban climate projections','Interdependence of critical infrastructure','Nature-based adaptation under constraints','Risk communication and public decisions','Spatial access under climate disruption','Recovery priorities for resilient services','Comparing early-warning design options','Land use change and flood vulnerability','Impact assessment for coastal adaptation','Quantifying cascade failures in supply chains','Equity dimensions of relocation policies','Resource allocation under climate shocks','Adaptation finance scenario modelling','Participatory resilience metrics','Monitoring green infrastructure outcomes','Evaluating climate risk governance']},
{key:'education',name:'Cross-Border Education',subtitle:'Mobility · access · language · institutions',seed:1197,
topics:['Mobility','Access','Language','Institutions'],titles:[
'Patterns of student mobility across borders','Digital access and educational opportunity','Multilingual learning in regional universities','Institutional cooperation networks','Student trajectories in border regions','Equity in remote higher education','Language practices of mobile learners','Governance of academic credit transfer','Educational aspirations and social networks','Geographical constraints in university choice','Academic writing across multiple languages','Cross-border recognition of qualifications','Evaluating virtual course accessibility','Educational experiences of commuters','Teacher networks across institutions','Language policy and inclusion','Barriers to research participation','Digital platforms in comparative education','Institutional responses to learner mobility','Social belonging among international students','Student support strategies across borders','Comparative access to doctoral programs']}
];
C.presets=PRESETS.map(x=>({key:x.key,name:x.name,subtitle:x.subtitle}));
C.makePreset=(key)=>{
const cfg=PRESETS.find(x=>x.key===key)||PRESETS[0];const rand=C.prng(cfg.seed),papers=[];
cfg.titles.forEach((title,i)=>{
const year=2014+Math.floor(i/2);let topic=cfg.topics[(i*3+Math.floor(i/6))%4];papers.push({id:'P'+String(i+1).padStart(2,'0'),title,year,topic,authors:`Research Team ${String.fromCharCode(65+i%12)} · ${String.fromCharCode(70+i%7)}`});
});
let citations=[],s=new Set();papers.forEach((p,i)=>{if(!i)return;const max=Math.min(4,1+Math.floor(rand()*4));let options=papers.slice(0,i).filter(q=>q.year<=p.year);options.sort((a,b)=>((a.topic===p.topic?-3:0)+(p.year-a.year)*.32)-((b.topic===p.topic?-3:0)+(p.year-b.year)*.32)||a.id.localeCompare(b.id));
for(let j=0;j<Math.min(max,options.length);j++){const t=options[j];const k=p.id+'|'+t.id;if(!s.has(k)){s.add(k);citations.push({source:p.id,target:t.id});}}
});return C.validate(papers,citations);
};
C.analyze=(raw)=>{
const data=C.validate(raw.papers,raw.citations),p=data.papers,e=data.citations,n=p.length,m=e.length;const index=new Map(p.map((v,i)=>[v.id,i]));
const indeg=Array(n).fill(0),outdeg=Array(n).fill(0),refs=p.map(()=>[]),citedBy=p.map(()=>[]),adj=p.map(()=>new Set());
for(const edge of e){let a=index.get(edge.source),b=index.get(edge.target);outdeg[a]++;indeg[b]++;refs[a].push(b);citedBy[b].push(a);adj[a].add(b);adj[b].add(a);}
let pr=Array(n).fill(1/n);for(let t=0;t<150;t++){let nx=Array(n).fill((1-.85)/n);let dangling=0;for(let i=0;i<n;i++){if(!outdeg[i])dangling+=pr[i];else for(const j of refs[i])nx[j]+=.85*pr[i]/outdeg[i];}for(let i=0;i<n;i++)nx[i]+=.85*dangling/n;let d=nx.reduce((s,x,i)=>s+Math.abs(x-pr[i]),0);pr=nx;if(d<1e-12)break;}
let components=[];const seen=new Set();for(let i=0;i<n;i++){if(seen.has(i))continue;let queue=[i],members=[];seen.add(i);while(queue.length){let u=queue.pop();members.push(u);for(let j of adj[u])if(!seen.has(j)){seen.add(j);queue.push(j);}}components.push(members);}
const topics=[...new Set(p.map(x=>x.topic))].sort(),topicMap=Object.fromEntries(topics.map(topic=>[topic,{topic,papers:0,citations:0,authors:new Set(),ids:[]} ]));
for(let i=0;i<n;i++){const o=topicMap[p[i].topic];o.papers++;o.citations+=indeg[i];o.ids.push(i);p[i].authors.split(/[;,·]/).map(a=>a.trim()).filter(Boolean).forEach(a=>o.authors.add(a));}
const years=[...new Set(p.map(x=>x.year))].sort((a,b)=>a-b).map(y=>({year:y,papers:p.filter(x=>x.year===y).length,citations:e.filter(x=>p[index.get(x.source)].year===y).length}));
const ranked=p.map((paper,i)=>({...paper,incoming:indeg[i],outgoing:outdeg[i],rank:pr[i],share:pr[i]*100})).sort((a,b)=>b.rank-a.rank||b.incoming-a.incoming);
const ins=indeg.slice().sort((a,b)=>b-a),h=ins.reduce((h,x,i)=>x>=i+1?i+1:h,0);
const pairList=[];for(let i=0;i<n;i++)for(let j=i+1;j<n;j++){const a=new Set(refs[i]),b=new Set(refs[j]);const shared=[...a].filter(v=>b.has(v)).length;if(shared>=2)pairList.push({a:p[i].id,b:p[j].id,shared,jaccard:shared/(a.size+b.size-shared)});}
pairList.sort((a,b)=>b.shared-a.shared||b.jaccard-a.jaccard);
const coCit=[];for(let i=0;i<n;i++)for(let j=i+1;j<n;j++){const a=new Set(citedBy[i]),b=new Set(citedBy[j]);const shared=[...a].filter(v=>b.has(v)).length;if(shared>=2)coCit.push({a:p[i].id,b:p[j].id,shared});}coCit.sort((a,b)=>b.shared-a.shared);
let concentration=0;if(m){const vals=indeg.slice().sort((a,b)=>a-b);let total=vals.reduce((a,b)=>a+b,0);concentration=vals.reduce((acc,x,i)=>acc+(2*(i+1)-n-1)*x,0)/(n*total);}
const audit=[];if(n<15)audit.push('Small graph: rankings may be unstable.');if(m<n)audit.push('Sparse graph: many papers may have no outgoing or incoming citations.');if(p.filter((x,i)=>indeg[i]===0).length/n>.5)audit.push('More than half the papers have no incoming citations in this dataset.');if(components.length>1)audit.push(components.length+' disconnected groups may limit comparisons.');if(Math.max(...indeg)/Math.max(1,m)>.4)audit.push('One publication receives more than 40% of citations.');
audit.push('Citations count only links within this uploaded corpus, not worldwide influence.');audit.push('Topic clusters are assigned from metadata, not inferred scientifically.');
return {data,index,indeg,outdeg,refs,citedBy,adj,pr,ranked,topics,topicMap,years,components,pairList,coCit,h,concentration,audit,metrics:{papers:n,citations:m,topics:topics.length,coverage:indeg.filter(x=>x>0).length/n,density:m/(n*(n-1)),largest:Math.max(...components.map(x=>x.length))/n,mean: m/n, maxIn:Math.max(...indeg)}};
};
C.focus=(analysis,id)=>{const i=analysis.index.get(id);if(i===undefined)return null;let p=analysis.data.papers[i];return {...p,incoming:analysis.indeg[i],outgoing:analysis.outdeg[i],pageRank:analysis.pr[i],references:analysis.refs[i].map(j=>analysis.data.papers[j]),citedBy:analysis.citedBy[i].map(j=>analysis.data.papers[j])};};
C.filter=(an,{topic='All topics',from=1950,to=2035}={})=>an.data.papers.filter(p=>(topic==='All topics'||p.topic===topic)&&p.year>=from&&p.year<=to);
C.exportCSV=an=>C.csv([['id','title','year','topic','authors','internal_citations','pagerank'],...an.data.papers.map((p,i)=>[p.id,p.title,p.year,p.topic,p.authors,an.indeg[i],an.pr[i].toFixed(9)])]);
C.citationCSV=an=>C.csv([['source','target'],...an.data.citations.map(e=>[e.source,e.target])]);
C.examples={papers:C.csv([['id','title','year','topic','authors'],['A1','A research paper',2021,'Methods','A. Author'],['A2','Further investigation',2022,'Methods','B. Author'],['A3','New evidence',2023,'Practice','C. Author']]),citations:C.csv([['source','target'],['A2','A1'],['A3','A1'],['A3','A2']])};
C.parseDataset=(paperCSV,citationCSV)=>C.validate(C.toRecords(paperCSV,['id','title','year','topic','authors']),C.toRecords(citationCSV,['source','target']));
if(typeof module!=='undefined'&&module.exports)module.exports=C;root.CGCore=C;
})(typeof window!=='undefined'?window:globalThis);
</script><script>/* CiteGraph presentation layer. No libraries, third-party APIs, or storage. */
(function(){'use strict';
const C=window.CGCore;
const el=id=>document.getElementById(id);
const esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const colors=['#67e7bd','#a7a5ff','#f3bc81','#81cafa','#f593a9','#d8b5f2'];
const fmt=v=>Number(v).toLocaleString('en-US');
const pct=(v,d=1)=>Number.isFinite(v)?(100*v).toFixed(d)+'%':'N/A';
const state={view:'overview',raw:C.makePreset('ai'),sample:true,preset:'ai',selected:'P01',topic:'All topics',search:'',minYear:1950,files:{papers:null,citations:null},analysis:null};
function analyze(){state.analysis=C.analyze(state.raw);if(!state.analysis.index.has(state.selected))state.selected=state.analysis.ranked[0].id;if(!state.analysis.topics.includes(state.topic))state.topic='All topics';}
analyze();
const LABEL={overview:['01 / RESEARCH OVERVIEW','Mapping the structure','of scientific knowledge.','Explore research connections, trace citations and reveal intellectual influence with auditable metrics.'],network:['02 / NETWORK EXPLORER','Follow the connections','behind the evidence.','Explore direction of citations, paper relationships and reference trails.'],influence:['03 / INFLUENCE RANKINGS','Measure influence','without guesswork.','Compare internal citation counts with PageRank, using transparent graph-based calculations.'],themes:['04 / THEMES & TIMELINE','Discover emerging','research directions.','Trace topics and connections over publication years in this collection.'],audit:['05 / AUDIT & METHODS','Understand every','analytical assumption.','Inspect data coverage, constraints and the exact interpretation of each reported metric.'],data:['06 / DATA & EXPORTS','Bring your own','research collection.','Import paper and citation CSVs. All analysis runs locally in your browser.']};
const tagStyle=(topic)=>`style="color:${col(topic)};border-color:${col(topic)}55"`;
function col(topic){const t=state.analysis.topics.indexOf(topic);return colors[(t<0?0:t)%colors.length]}
function toast(msg,error=false){const t=el('toast');t.textContent=msg;t.className='toast show'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(()=>t.className='toast',4500)}
const pill=(t)=>`<span class="pill">${esc(t)}</span>`;
function card(title,subtitle,content,right='',bodyClass=''){return `<section class="panel"><div class="panel-head"><div><h2>${title}</h2>${subtitle?`<p>${subtitle}</p>`:''}</div>${right}</div><div class="panel-body ${bodyClass}">${content}</div></section>`}
function kpi(label,value,info,mark){return `<div class="kpi"><div class="label">${label}</div><span class="mark">${mark}</span><strong>${value}</strong><div class="context">${info}</div></div>`}
function metricCards(){const m=state.analysis.metrics;return `<div class="kpis">${kpi('DOCUMENTS',fmt(m.papers),'Scholarly records in corpus','▤')}${kpi('CITATION LINKS',fmt(m.citations),'Directed internal relationships','↗')}${kpi('CITATION COVERAGE',pct(m.coverage),'Papers cited at least once','◉')}${kpi('RESEARCH TOPICS',fmt(m.topics),'Metadata-defined categories','◈')}</div>`}
function topicBars(){let a=state.analysis,ls=Object.values(a.topicMap).sort((a,b)=>b.papers-a.papers),max=Math.max(...ls.map(x=>x.papers));return `<div class="bars">${ls.map(item=>`<div class="bar-row"><div class="bar-info"><b>${esc(item.topic)}</b><span>${item.papers} papers · ${item.citations} cites</span></div><div class="bar-bg"><div class="bar-fill" style="width:${item.papers/max*100}%;background:${col(item.topic)}"></div></div></div>`).join('')}</div>`}
function rankedList(limit=8){const a=state.analysis;return `<div class="rank-list" role="list">${a.ranked.slice(0,limit).map((p,i)=>`<div class="rank-row" role="listitem" tabindex="0" data-paper="${esc(p.id)}"><span class="rank-number">${String(i+1).padStart(2,'0')}</span><div><div class="rank-title">${esc(p.title)}</div><div class="rank-sub">${esc(p.topic)} · ${p.year}</div></div><div class="right"><span class="rank-score">${p.incoming}</span><div class="rank-sub">INBOUND</div></div><div class="right"><span class="rank-score">${p.share.toFixed(2)}%</span><div class="rank-sub">PAGERANK</div></div></div>`).join('')}</div>`}
function graphSVG(height=440){const a=state.analysis,p=a.data.papers,n=p.length,topicNums=new Map(a.topics.map((x,i)=>[x,i]));let layout=new Map();
let groups=a.topics.map(t=>p.filter(x=>x.topic===t));groups.forEach((g,ti)=>{let centers=[[242,127],[592,127],[242,346],[592,346]];let center=centers[ti%4];g.forEach((paper,i)=>{let radius= i===0?0:(i<5?53:89),theta=2*Math.PI*(i-1)/Math.max(1,g.length-1)+ti*.39;let x=center[0]+Math.cos(theta)*radius*1.25,y=center[1]+Math.sin(theta)*radius*.87;layout.set(paper.id,{x,y});});});
let valid=new Set(C.filter(a,{topic:state.topic} ).filter(x=>x.title.toLowerCase().includes(state.search.toLowerCase())||x.authors.toLowerCase().includes(state.search.toLowerCase())||x.id.toLowerCase().includes(state.search.toLowerCase())).map(x=>x.id));
let edges=a.data.citations.map(e=>{const from=layout.get(e.source),to=layout.get(e.target),vis=valid.has(e.source)&&valid.has(e.target);return `<path class="graph-edge ${vis?'':'dim'}" d="M${from.x.toFixed(1)} ${from.y.toFixed(1)} L${to.x.toFixed(1)} ${to.y.toFixed(1)}" marker-end="url(#arrow)"/>`}).join('');
let nodes=p.map((paper,i)=>{let pt=layout.get(paper.id),active=state.selected===paper.id,shown=valid.has(paper.id),r=7+Math.sqrt(a.indeg[i])*2.5;return `<g class="graph-point graph-node ${shown?'':'dim'}" data-paper="${esc(paper.id)}" role="button" tabindex="0" aria-label="Select ${esc(paper.title)}"><title>${esc(paper.title)} · ${a.indeg[i]} internal citations</title><circle cx="${pt.x.toFixed(1)}" cy="${pt.y.toFixed(1)}" r="${(active?r+3:r).toFixed(1)}" fill="${col(paper.topic)}" stroke="${active?'#fff':'#0e1727'}" stroke-width="${active?3.5:2}"/><text x="${pt.x.toFixed(1)}" y="${(pt.y+r+16).toFixed(1)}" text-anchor="middle" opacity="${active||a.indeg[i]>=3?'1':'0.72'}">${esc(paper.id)}</text></g>`}).join('');
return `<svg viewBox="0 0 840 475" role="img" aria-label="Interactive directed citation graph with ${n} papers and ${a.data.citations.length} links" style="max-height:${height}px"><defs><marker id="arrow" markerWidth="5" markerHeight="5" refX="5" refY="2.5" orient="auto"><path d="M0 0L5 2.5L0 5" fill="#6093a0"/></marker></defs><path d="M420 40 V440 M80 233 H760" stroke="#213648" stroke-dasharray="4 7" fill="none"/>${edges}${nodes}</svg>`;
}
function graphPanel(big=false){let sub=big?'Select a paper to inspect its references and citing publications':'Node radius is proportional to internal citations';return `<section class="panel"><div class="panel-head"><div><h2>Scholarly citation network</h2><p>${sub}</p></div>${pill(state.analysis.data.papers.length+' NODES')}</div>${big?`<div class="panel-body"><div class="toolbar"><input class="field-input" id="graphSearch" value="${esc(state.search)}" placeholder="Search title, authors or ID"><select id="topicSelect" class="mini-select"><option>All topics</option>${state.analysis.topics.map(t=>`<option ${state.topic===t?'selected':''}>${esc(t)}</option>`).join('')}</select><button class="btn btn-outline" data-action="resetFilter">Reset</button></div></div>`:''}<div class="graph-stage">${graphSVG(big?570:430)}</div><div class="graph-key">${state.analysis.topics.map(t=>`<span><b style="background:${col(t)}"></b>${esc(t)}</span>`).join('')}</div></section>`}
function focusDetails(){let f=C.focus(state.analysis,state.selected);return `<div><div class="section-label">SELECTED RESEARCH PAPER</div><h3 class="details-title">${esc(f.title)}</h3><div class="details-meta">${esc(f.authors)}<br>${f.year} · ${esc(f.id)} · ${esc(f.topic)}</div><div class="tiny-grid" style="margin-top:15px"><div class="mini-stat"><small>Citations received</small><strong>${f.incoming}</strong></div><div class="mini-stat"><small>References made</small><strong>${f.outgoing}</strong></div><div class="mini-stat"><small>PageRank share</small><strong>${pct(f.pageRank,2)}</strong></div><div class="mini-stat"><small>Corpus only</small><strong>YES</strong></div></div><div class="section-label">CITES THESE PAPERS (${f.references.length})</div><div class="details-links">${f.references.length?f.references.map(q=>`<button class="link-item" data-paper="${esc(q.id)}">${esc(q.title)}</button>`).join(''):'<div class="small-muted">No outgoing references in this corpus.</div>'}</div><div class="section-label">CITED BY (${f.citedBy.length})</div><div class="details-links">${f.citedBy.length?f.citedBy.map(q=>`<button class="link-item" data-paper="${esc(q.id)}">${esc(q.title)}</button>`).join(''):'<div class="small-muted">No incoming citations in this corpus.</div>'}</div></div>`}
function overview(){return `${metricCards()}<div class="grid-2">${graphPanel()}${card('Topic distribution','Documents grouped by supplied topic labels',topicBars(),pill('4 THEMES'))}</div><div class="grid-half">${card('Leading publications','Ranked by PageRank within this dataset',rankedList(5),'<button class="btn btn-soft" data-view="influence">View all →</button>')}${card('Collection snapshot','Descriptive statistics, not global citation counts',`<div class="tiny-grid"><div class="mini-stat"><small>Directed density</small><strong>${pct(state.analysis.metrics.density,2)}</strong></div><div class="mini-stat"><small>Weakly connected</small><strong>${pct(state.analysis.metrics.largest)}</strong></div><div class="mini-stat"><small>Internal h-index</small><strong>${state.analysis.h}</strong></div><div class="mini-stat"><small>Citation Gini</small><strong>${state.analysis.concentration.toFixed(2)}</strong></div></div><div class="divider"></div><div class="notice">Demo publications, authors and citation relationships are <b>fictional</b>. Import verified data to analyze a real literature collection.</div>`,'<span class="pill muted">LOCAL ONLY</span>')}</div>`}
function network(){return `<div class="grid-2">${graphPanel(true)}${card('Paper inspector','Select a node or title to explore relationships',focusDetails(),pill('LIVE'))}</div>`}
function influence(){const a=state.analysis;const max=Math.max(...a.ranked.map(x=>x.rank));return `${metricCards()}<div class="grid-2">${card('Publication influence','PageRank weights direction of references',`<div class="bars">${a.ranked.slice(0,12).map((p,i)=>`<div class="bar-row"><div class="bar-info"><b>${i+1}. ${esc(p.title)}</b><span>${pct(p.rank,2)}</span></div><div class="bar-bg"><div class="bar-fill" style="width:${p.rank/max*100}%;background:${col(p.topic)}"></div></div></div>`).join('')}</div>`,pill('DAMPING 0.85'))}${card('Most cited in corpus','Raw inbound citations (not worldwide)',rankedList(9),pill('IN-DEGREE'))}</div>${card('Ranking interpretation','Understanding influence responsibly',`<div class="notice">PageRank is a network score, not a measure of scientific quality. An older paper may accumulate more citations than a recent one; incomplete datasets distort comparisons.</div><div class="tiny-grid" style="margin-top:14px"><div class="mini-stat"><small>Rank damping</small><strong>0.85</strong></div><div class="mini-stat"><small>Sum of PageRank</small><strong>${a.pr.reduce((s,v)=>s+v,0).toFixed(3)}</strong></div></div>`)}`}
function timeline(){const ys=state.analysis.years,max=Math.max(...ys.map(x=>x.papers)),w=740,h=260,barW=(w-64)/Math.max(ys.length,1);let bars=ys.map((x,i)=>{let height=x.papers/max*162,left=39+i*barW,top=198-height;return `<rect x="${left}" y="${top}" width="${Math.max(7,barW-7)}" height="${height}" rx="3" fill="${i%2===0?'#67e5bc':'#9a9af4'}"/><text x="${left+(barW-7)/2}" y="${Math.max(15,top-7)}" text-anchor="middle" fill="#d2e0e9" font-size="12">${x.papers}</text><text x="${left+(barW-7)/2}" y="221" text-anchor="middle" fill="#a9b8c6" font-size="11">${x.year}</text>`}).join('');return `<svg class="timeline" viewBox="0 0 740 245" role="img" aria-label="Publications by year"><path d="M20 198H720" stroke="#607185"/>${bars}</svg>`}
function themes(){const a=state.analysis;return `<div class="model-grid">${Object.values(a.topicMap).map(t=>`<div class="topic-card"><div class="top">${esc(t.topic)}</div><div class="row between"><div><div class="big">${t.papers}</div><div class="muted">publications</div></div><div><div class="big">${t.citations}</div><div class="muted">internal cites</div></div></div><div class="stripe" style="background:${col(t.topic)}"></div></div>`).join('')}</div><div class="grid-half">${card('Publication timeline','Counts per publication year',timeline(),pill('YEARLY'))}${card('Bibliographic coupling','Pairs sharing at least two references',`<div class="rank-list">${a.pairList.length?a.pairList.slice(0,8).map((v,i)=>`<div class="rank-row"><span class="rank-number">${i+1}</span><div><div class="rank-title">${esc(v.a)} ↔ ${esc(v.b)}</div><div class="rank-sub">Shared references</div></div><div class="right"><span class="rank-score">${v.shared}</span></div><div class="right"><span class="rank-score">${pct(v.jaccard,0)}</span></div></div>`).join(''):'<div class="empty">No pairs meet the minimum shared reference count.</div>'}</div>`,pill('JACCARD'))}</div>${card('Co-citation proximity','Papers cited together by other publications',`<div class="tag-row">${a.coCit.length?a.coCit.slice(0,12).map(x=>`<span class="tag">${esc(x.a)} + ${esc(x.b)} · ${x.shared} shared citing papers</span>`).join(''):'<div class="empty">No co-citation pairs with two or more common citing papers.</div>'}</div><p class="panel-caption">Relationships refer to this corpus only. They are structural similarities, not proof of the same research findings.</p>`)}`}
function audit(){let a=state.analysis;return `<div class="grid-half">${card('Data quality & interpretability','Automatic diagnostics for the current corpus',`<div class="audit-list">${a.audit.map(x=>`<div class="audit-item"><span>◇</span><div>${esc(x)}</div></div>`).join('')}</div>`,pill('REVIEW'))}${card('Dataset integrity','Validated on import and before analysis',`<div class="tiny-grid"><div class="mini-stat"><small>Unique paper IDs</small><strong>${fmt(a.metrics.papers)}</strong></div><div class="mini-stat"><small>Valid links</small><strong>${fmt(a.metrics.citations)}</strong></div><div class="mini-stat"><small>Connected groups</small><strong>${a.components.length}</strong></div><div class="mini-stat"><small>Max inbound citations</small><strong>${a.metrics.maxIn}</strong></div></div><div class="divider"></div><p class="section-legend">Imports reject duplicate references, dangling links, self-citations, invalid dates and citations to papers published in the future.</p>`,pill('VALIDATED'))}</div>${card('Methodology and limits','Definitions needed for research integrity',`<div class="audit-list"><div class="audit-item"><span>01</span><div><b>Directed edge</b><br>source = citing paper, target = referenced paper.</div></div><div class="audit-item"><span>02</span><div><b>Internal citation count</b><br>Number of incoming edges, excluding references from outside this corpus.</div></div><div class="audit-item"><span>03</span><div><b>PageRank</b><br>Iterative score with damping 0.85 and uniform redistribution from nodes without references.</div></div><div class="audit-item"><span>04</span><div><b>Bibliographic coupling and co-citation</b><br>Common references versus common papers citing a pair. Both are calculated on this subset.</div></div><div class="audit-item"><span>05</span><div><b>Limitations</b><br>Selection bias, missing references, chronology and metadata errors can distort all results. Topics are labels provided by the data source.</div></div></div>`)}`}
function dataView(){return `<div class="grid-half">${card('Import your collection','Upload both CSV files; no data leaves your device',`<div class="file-box"><strong>1. Papers CSV</strong><input type="file" id="papersFile" accept=".csv,text/csv" aria-label="Papers CSV"><p class="field-note">Required columns: <span class="mono">id, title, year, topic, authors</span></p></div><div class="file-box"><strong>2. Citations CSV</strong><input type="file" id="citationsFile" accept=".csv,text/csv" aria-label="Citations CSV"><p class="field-note">Required columns: <span class="mono">source, target</span></p></div><div class="row wrap"><button class="btn btn-primary" id="importBtn">↥ Import & validate</button><button class="btn btn-outline" data-action="resetDemo">Reset example</button></div><p class="field-note">Limits: 3–120 papers, 600 citations, 1.5 MB per CSV. Every link must refer to a known paper.</p>`,pill('NO UPLOADS'))}${card('Exports & templates','Download reproducible results for offline review',`<div class="action-grid"><button class="btn btn-outline" data-download="report">↓ Full audit JSON</button><button class="btn btn-outline" data-download="papers">↓ Papers + metrics CSV</button><button class="btn btn-outline" data-download="citations">↓ Citation links CSV</button><button class="btn btn-outline" data-download="markdown">↓ Research summary MD</button><button class="btn btn-outline" data-download="templatePapers">↓ Papers template CSV</button><button class="btn btn-outline" data-download="templateCitations">↓ Links template CSV</button></div><div class="divider"></div><div class="notice"><b>Privacy:</b> All calculations are in-browser. No external APIs, cookies or telemetry. Refresh to restore the demonstration dataset.</div>`,pill('REPRODUCIBLE'))}</div>${card('CSV data contract','Supported schema and example content',`<div class="grid-half" style="margin:0"><div><div class="section-label">PAPERS.CSV</div><div class="code-snippet">id,title,year,topic,authors\nA1,A research paper,2021,Methods,A. Author\nA2,Further investigation,2022,Methods,B. Author</div></div><div><div class="section-label">CITATIONS.CSV</div><div class="code-snippet">source,target\nA2,A1\nA3,A1\nA3,A2</div></div></div><p class="panel-caption">The sample templates contain 3 papers (including A3) and the links above. UTF-8 CSV with a header row, quoted fields supported. Citations can reference same-year or older papers.</p>`)}`}
function report(){let a=state.analysis;return {application:'CiteGraph 1.0',generatedAt:new Date().toISOString(),synthetic:state.sample,description:'All counts and rankings refer only to the supplied citation corpus.',methodology:{pagerankDamping:0.85,topics:'supplied metadata',citationCount:'internal directed in-degree'},metrics:a.metrics,internalHIndex:a.h,citationGini:a.concentration,ranked:a.ranked,topics:Object.values(a.topicMap).map(({authors,ids,...rest})=>({...rest,authors:[...authors]})),audit:a.audit,papers:a.data.papers,citations:a.data.citations};}
function mkSummary(){let a=state.analysis;return `# CiteGraph · Scholarly Citation Network Audit\n\nGenerated: ${new Date().toISOString()}\n\nData: ${state.sample?'SYNTHETIC DEMONSTRATION':'USER-IMPORTED'}\n\n- Papers: ${a.metrics.papers}\n- Internal citation links: ${a.metrics.citations}\n- Citation coverage: ${pct(a.metrics.coverage)}\n- PageRank damping: 0.85\n- Internal h-index: ${a.h}\n\n## Leading papers\n${a.ranked.slice(0,10).map((p,i)=>`${i+1}. ${p.title.replace(/[\r\n]/g,' ')} (${p.year}), ${p.incoming} internal citations; ${pct(p.rank,2)} PageRank.`).join('\n')}\n\n## Audit notes\n${a.audit.map(x=>'- '+x).join('\n')}\n\n**Methodological caution:** Internal citation links are not total academic citations. Ranking does not imply paper quality or external validity.\n`;}
function download(text,name,mime){let blob=new Blob([text],{type:mime}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.hidden=true;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1300);toast('Downloaded '+name);}
function doDownload(t){const a=state.analysis;switch(t){case'report':download(JSON.stringify(report(),null,2),'citegraph-audit.json','application/json');break;case'papers':download(C.exportCSV(a),'citegraph-publications.csv','text/csv;charset=utf-8');break;case'citations':download(C.citationCSV(a),'citegraph-citations.csv','text/csv;charset=utf-8');break;case'markdown':download(mkSummary(),'citegraph-research-summary.md','text/markdown;charset=utf-8');break;case'templatePapers':download(C.examples.papers,'citegraph-papers-template.csv','text/csv;charset=utf-8');break;case'templateCitations':download(C.examples.citations,'citegraph-citations-template.csv','text/csv;charset=utf-8');break;}}
function render(){const label=LABEL[state.view];el('eyebrow').textContent=label[0];el('pageTitle').innerHTML=esc(label[1])+'<br><em>'+esc(label[2])+'</em>';el('pageSubtitle').textContent=label[3];el('breadcrumb').textContent=state.view.toUpperCase();el('datasetCaption').textContent=state.sample?'SYNTHETIC SAMPLE · REPRODUCIBLE':'IMPORTED DATA · LOCAL ONLY';el('presetSelect').value=state.sample?state.preset:'ai';el('presetSelect').disabled=!state.sample;document.querySelectorAll('.nav-link').forEach(x=>{x.classList.toggle('active',x.dataset.view===state.view);x.setAttribute('aria-current',x.dataset.view===state.view?'page':'false')});
let html={overview,network,influence,themes,audit,data:dataView}[state.view]();el('content').innerHTML=html;}
function changeView(v){if(!LABEL[v])return;state.view=v;state.search='';state.topic='All topics';render();el('sidebar').classList.remove('open');el('menuBtn').setAttribute('aria-expanded','false');window.scrollTo({top:0,behavior:'instant'});}
async function importFiles(){let f1=state.files.papers||el('papersFile')?.files[0],f2=state.files.citations||el('citationsFile')?.files[0];if(!f1||!f2){toast('Choose both papers.csv and citations.csv files first.',true);return;}if(f1.size>1500000||f2.size>1500000){toast('Maximum size: 1.5 MB per file.',true);return;}let btn=el('importBtn');if(btn)btn.disabled=true;try{let texts=await Promise.all([f1.text(),f2.text()]);let incoming=C.parseDataset(texts[0],texts[1]);state.raw=incoming;state.sample=false;state.selected=incoming.papers[0].id;state.files={papers:null,citations:null};state.topic='All topics';state.search='';analyze();render();toast(`Loaded ${incoming.papers.length} papers and ${incoming.citations.length} citations successfully.`);}catch(e){toast('Import rejected: '+e.message,true);if(btn)btn.disabled=false;}}
function handleClick(event){let n=event.target.closest('[data-view]');if(n){changeView(n.dataset.view);return;}let p=event.target.closest('[data-paper]');if(p){state.selected=p.dataset.paper; if(state.view==='overview')state.view='network';render();return;}const dl=event.target.closest('[data-download]');if(dl){doDownload(dl.dataset.download);return;}const ac=event.target.closest('[data-action]');if(ac){if(ac.dataset.action==='resetFilter'){state.search='';state.topic='All topics';render();}else if(ac.dataset.action==='resetDemo'){state.raw=C.makePreset('ai');state.preset='ai';state.sample=true;state.selected='P01';state.topic='All topics';state.search='';analyze();render();toast('Demonstration dataset restored.');}return;}if(event.target.id==='importBtn'){importFiles();}}
document.addEventListener('click',handleClick);
document.addEventListener('keydown',e=>{const t=e.target.closest('[data-paper]');if(t&&(e.key==='Enter'||e.key===' ')){e.preventDefault();state.selected=t.dataset.paper;if(state.view==='overview')state.view='network';render();}});
document.addEventListener('input',e=>{if(e.target.id==='graphSearch'){state.search=e.target.value;const value=state.search;render();let f=el('graphSearch');if(f){f.focus();f.setSelectionRange(value.length,value.length);}}});
document.addEventListener('change',e=>{if(e.target.id==='presetSelect'){state.preset=e.target.value;state.raw=C.makePreset(state.preset);state.sample=true;state.selected='P01';state.search='';state.topic='All topics';analyze();render();toast('Collection changed. All metrics recalculated.');}else if(e.target.id==='topicSelect'){state.topic=e.target.value;render();}else if(e.target.id==='papersFile'){state.files.papers=e.target.files[0]||null;}else if(e.target.id==='citationsFile'){state.files.citations=e.target.files[0]||null;}});
el('downloadReport').addEventListener('click',()=>doDownload('report'));
el('menuBtn').addEventListener('click',()=>{let s=el('sidebar'),now=s.classList.toggle('open');el('menuBtn').setAttribute('aria-expanded',String(now));});
render();window.CiteGraphTest={state,analyze,report,summary:mkSummary};
})();
</script>
</body></html>