| <!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="#102b35"><title>ScholarGraph | Research Collaboration Intelligence</title> |
| <style> |
| :root{--ink:#16313e;--muted:#75909f;--sidebar:#102c36;--sidebar2:#163b46;--accent:#087c79;--accent2:#57beaa;--lime:#b4e1bc;--paper:#f3f7f8;--card:#fff;--border:#dce8eb;--soft:#e9f7f3;--warning:#cd863a;--danger:#be5858;--shadow:0 16px 44px rgba(19,50,65,.05);--radius:17px;color-scheme:light} |
| *{box-sizing:border-box}body{margin:0;background:var(--paper);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);font-size:13px}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:3px solid #e3b467;outline-offset:2px}svg{max-width:100%}a{color:var(--accent)}.shell{display:flex;min-height:100vh}.sidebar{width:244px;background:var(--sidebar);color:#d3e6e9;position:fixed;top:0;left:0;bottom:0;padding:27px 15px 18px;display:flex;flex-direction:column;z-index:7}.brand{display:flex;align-items:center;gap:12px;padding:4px 7px 35px}.brandmark{width:45px;height:45px;background:linear-gradient(145deg,#b2eed5,#55c3b4);color:#0c4a46;border-radius:13px;display:grid;place-items:center;font-weight:900;font-size:26px;box-shadow:0 12px 20px rgba(1,10,10,.2)}.brand strong{color:#fff;display:block;letter-spacing:-.7px;font-size:19px}.brand small{font-size:9px;letter-spacing:2px;line-height:2.1;color:#91b8bb;font-weight:700}.sideLabel{letter-spacing:2px;color:#84a8b0;text-transform:uppercase;font-weight:800;font-size:9px;margin:0 11px 12px}.nav{display:flex;flex-direction:column;gap:7px}.nav button{display:flex;align-items:center;text-align:left;gap:11px;background:transparent;color:#a9c6cb;border:0;padding:13px 13px;width:100%;border-radius:10px;font-weight:650}.nav button:hover{background:#204752;color:#fff}.nav button.active{color:#fff;background:#1d4852;box-shadow:inset 3px 0 #a1eed2}.nav .symbol{font-size:17px;width:21px;text-align:center}.sidefoot{margin-top:auto;background:#1b4650;border:1px solid #33606a;border-radius:13px;padding:14px;font-size:11px;line-height:1.6;color:#b2d5d6}.sidefoot strong{color:#dbfff0}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#a5e6c7;margin-right:5px}.main{margin-left:244px;padding:27px 37px 23px;width:calc(100% - 244px);max-width:1830px}.top{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:27px}.crumb{font-size:11px;color:#7192a0;font-weight:700}.crumb b{color:#17303e}.toolbar{display:flex;gap:9px;align-items:center;flex-wrap:wrap}.btn{padding:10px 13px;border:1px solid var(--border);background:var(--card);border-radius:9px;color:var(--ink);font-weight:750;font-size:11px;white-space:nowrap}.btn:hover{border-color:#84bab4;background:var(--soft)}.btn.primary{color:white;background:var(--accent);border-color:var(--accent)}.btn.primary:hover{background:#0b6261}.btn.danger{color:#b04c4c}.btn.mini{font-size:10px;padding:7px 10px}.header{display:flex;justify-content:space-between;gap:20px;align-items:start;margin-bottom:23px}.eyebrow{text-transform:uppercase;color:var(--accent);font-size:10px;letter-spacing:1.8px;font-weight:850;margin-bottom:7px}h1{font-size:clamp(27px,3vw,39px);letter-spacing:-1.8px;line-height:1.1;margin:0 0 8px}h2{font-size:16px;letter-spacing:-.3px;margin:0}h3{font-size:14px;margin:0}p{margin:7px 0;color:var(--muted);line-height:1.55}.subtitle{max-width:700px;font-size:13px}.pill{white-space:nowrap;background:#e6f6ee;color:#088168;padding:8px 11px;border-radius:30px;font-size:10px;font-weight:800}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:13px;margin-bottom:15px}.kpi{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;box-shadow:var(--shadow);min-height:126px}.kpiTop{display:flex;justify-content:space-between;align-items:center;font-weight:700;font-size:11px;color:#688996}.kpiIcon{display:grid;place-items:center;background:#e7f7f0;width:32px;height:32px;border-radius:9px;color:#168878;font-size:16px}.kpi strong{display:block;font-size:30px;letter-spacing:-1.4px;margin:11px 0 3px}.kpi p{margin:0;font-size:10px}.grid{display:grid;grid-template-columns:1.65fr 1fr;gap:15px;margin-bottom:15px}.grid.equal{grid-template-columns:1fr 1fr}.panel{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:19px;box-shadow:var(--shadow);min-width:0}.panelHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:15px}.panelHead p{font-size:11px;margin:5px 0 0}.labelTag{font-size:10px;background:#e4f5f0;color:#15846d;padding:7px 9px;border-radius:9px;white-space:nowrap;font-weight:750}.network{border:1px solid #e8f0f2;border-radius:13px;background:radial-gradient(circle at 50% 50%,#ffffff,#f6fafa);min-height:365px;position:relative;overflow:hidden}.network svg{width:100%;height:365px;display:block;touch-action:manipulation}.network .node{cursor:pointer}.network .node:hover circle{stroke:#4faba7;stroke-width:5}.legend{display:flex;flex-wrap:wrap;gap:15px;color:#728f9b;font-size:10px;margin-top:12px}.legend span{display:flex;align-items:center;gap:6px}.legend i{width:9px;height:9px;border-radius:50%;display:inline-block}.networkTip{position:absolute;bottom:12px;left:13px;background:#fffdf5;border:1px solid #e7eada;border-radius:8px;padding:7px 10px;color:#628189;font-size:10px}.ranking{display:flex;flex-direction:column;gap:0}.rank{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--border)}.rank:last-child{border:0}.rankno{width:25px;height:25px;border-radius:8px;display:grid;place-items:center;background:#e6f4f1;color:#147e70;font-weight:850;font-size:11px}.avatar{width:34px;height:34px;display:grid;place-items:center;color:#fff;border-radius:11px;font-weight:850;flex-shrink:0;font-size:12px}.rankname{font-size:12px;font-weight:800}.ranksub{color:var(--muted);font-size:10px;margin-top:3px}.rankmetric{margin-left:auto;text-align:right;min-width:65px}.rankmetric b{font-size:16px}.rankmetric small{display:block;font-size:9px;color:#7b9ba6}.progress{height:7px;border-radius:8px;background:#eff4f5;overflow:hidden;flex:1;margin-left:4px;max-width:110px}.progress div{height:100%;background:linear-gradient(90deg,#11918c,#9bd5b5);border-radius:8px}.hint{border:1px solid #cde9df;background:#e9f7f1;padding:13px;border-radius:11px;color:#436b69;font-size:11px;line-height:1.55}.hint b{color:#137970}.miniRow{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:11px 0;border-bottom:1px solid #e9eff1;font-size:11px}.miniRow:last-child{border:0}.miniRow strong{font-size:13px}.bartrack{height:9px;border-radius:9px;background:#f0f4f5;flex:1;margin:0 14px;overflow:hidden}.bartrack div{height:100%;border-radius:9px;background:linear-gradient(90deg,#168f8d,#aad7b5)}.footnote{margin-top:16px;color:#83a0a9;font-size:10px}.page{display:none}.page.active{display:block}.input,.select{border:1px solid #d6e4e7;border-radius:9px;background:var(--card);color:var(--ink);padding:10px 11px;min-width:0;width:100%}.input:focus,.select:focus{outline:2px solid #81ccc0}.field{display:flex;flex-direction:column;gap:6px;min-width:0}.field label{color:#63838f;font-size:10px;font-weight:850}.controls{display:flex;flex-wrap:wrap;gap:11px;align-items:end;margin-bottom:18px}.controls .field{min-width:160px;flex:1}.statline{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.stat{padding:13px;border-radius:11px;background:#f6f9fa;border:1px solid var(--border)}.stat small{color:var(--muted);font-size:10px;display:block}.stat strong{display:block;font-size:22px;letter-spacing:-1px;margin:5px 0 2px}.chart{width:100%;display:block;height:auto}.tableWrap{width:100%;overflow-x:auto;border:1px solid var(--border);border-radius:12px}table{width:100%;border-collapse:collapse;font-size:11px}th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.8px;background:#f6f9fa;color:#78939e;padding:13px 12px;white-space:nowrap}td{padding:13px 12px;border-top:1px solid var(--border);white-space:nowrap}td b{font-size:11px}.actioncell{display:flex;gap:6px}tr.inactive{opacity:.48}.tag{padding:5px 8px;border-radius:8px;font-size:10px;font-weight:750;background:#e6f5ec;color:#207d65}.tag.archived{background:#f3eded;color:#9d6767}.tag.bridge{background:#fff4e4;color:#aa7136}.stack{display:flex;flex-direction:column;gap:12px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.thirds{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.timeline{display:flex;gap:8px;margin-top:17px}.timeline .step{flex:1;background:#e9f2f3;border-radius:8px;min-height:53px;padding:10px 9px;color:#2f6770;font-size:10px;font-weight:750;display:flex;align-items:center;justify-content:space-between;gap:8px}.timeline .step b{font-size:17px}.timeline .step.good{background:#e2f4ed;color:#087b62}.modalShade{position:fixed;inset:0;background:rgba(10,31,39,.58);z-index:20;display:none;align-items:center;justify-content:center;padding:15px}.modalShade.show{display:flex}.modal{width:min(540px,100%);max-height:92vh;overflow-y:auto;background:var(--card);border-radius:19px;padding:23px;box-shadow:0 25px 90px rgba(0,0,0,.3)}.modalHeader{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:17px}.formGrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.formGrid .full{grid-column:1/-1}.modalActions{display:flex;gap:9px;justify-content:flex-end;margin-top:21px}.notice{min-height:28px;padding:8px 12px;color:#227969;border-radius:8px;font-size:11px;display:none;background:#e4f4ed}.notice.show{display:block}.textBlock{color:#50717c;line-height:1.75;font-size:12px}.textBlock b{color:var(--ink)}.textBlock code{font-size:11px;background:#edf3f4;padding:3px 5px;border-radius:5px}.methodList{padding-left:19px}.methodList li{margin-bottom:13px}.mobileTop{display:none}.hiddenFile{display:none}.empty{padding:24px;text-align:center;color:var(--muted)} |
| body.dark{--ink:#dbeeea;--muted:#a0b8bd;--paper:#10232c;--card:#19343e;--border:#32515b;--soft:#214e50;--accent:#60c9b1;--shadow:none;--sidebar:#081c25;--sidebar2:#1b3b43;color-scheme:dark}body.dark .network{background:radial-gradient(circle at 50% 50%,#203b43,#18313b);border-color:#36555d}body.dark .networkTip{background:#213d40;color:#c4dddf;border-color:#3b6863}body.dark .rankno,body.dark .kpiIcon,body.dark .labelTag,body.dark .hint,body.dark .stat,body.dark .tag,body.dark .step,body.dark .tag.bridge{background:#284b4d;color:#a2e9d4;border-color:#396c64}body.dark th,body.dark .btn,body.dark .input,body.dark .select{background:#20414b}body.dark .bartrack,body.dark .progress{background:#2d4a54}body.dark .hint b,body.dark .textBlock b{color:#9ce6d4}body.dark .textBlock code{background:#28434a} |
| @media(max-width:1200px){.main{padding:23px}.grid{grid-template-columns:1.3fr 1fr}.progress{display:none}}@media(max-width:1000px){.sidebar{width:205px}.main{margin-left:205px;width:calc(100% - 205px);padding:22px}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.grid{grid-template-columns:1fr}.progress{display:block}}@media(max-width:720px){.sidebar{display:none}.mobileTop{display:flex;position:sticky;top:0;z-index:10;background:var(--sidebar);padding:12px 16px;align-items:center;gap:9px;overflow-x:auto;color:#fff}.mobileTop .brandmini{font-weight:850;margin-right:6px;flex-shrink:0}.mobileTop button{background:#254955;border:1px solid #416771;border-radius:8px;padding:7px 10px;color:#deeff0;font-size:10px;white-space:nowrap;font-weight:700}.mobileTop button.active{background:#b0e8d2;color:#154a47}.shell{display:block}.main{width:100%;margin-left:0;padding:19px 15px}.top{align-items:start;flex-direction:column;margin-bottom:22px}.header{flex-direction:column;gap:11px}.kpis{gap:9px}.kpi{padding:13px;min-height:110px}.kpi strong{font-size:26px}.panel{padding:14px}.two,.thirds,.grid.equal{grid-template-columns:1fr}.statline{gap:6px}.stat{padding:10px}.stat strong{font-size:18px}.formGrid{grid-template-columns:1fr}.formGrid .full{grid-column:1}.network{min-height:280px}.network svg{height:280px}.controls .field{min-width:120px}.toolbar .btn{padding:9px}.footnote{line-height:1.55}}@media(max-width:380px){.kpiTop{font-size:9px}.kpi strong{font-size:23px}.stat small{font-size:9px}.stat strong{font-size:17px}.brandmini{font-size:12px}.panelHead{flex-wrap:wrap}} |
| </style> |
| </head> |
| <body><div class="mobileTop" id="mobileNav"><span class="brandmini">β£ ScholarGraph</span></div><div class="shell"><aside class="sidebar"><div class="brand"><div class="brandmark">β£</div><div><strong>ScholarGraph</strong><small>RESEARCH INTELLIGENCE</small></div></div><div class="sideLabel">WORKSPACE</div><nav class="nav" id="nav"></nav><div class="sidefoot"><strong><span class="dot"></span> Private demo workspace</strong><br>Fictional researchers and collaborations. Calculated locally, with no external APIs or tracking.</div></aside><main class="main"><div class="top"><span class="crumb">Hugdrigo / Research portfolio / <b>Project #111</b></span><div class="toolbar"><button class="btn" data-action="theme" title="Toggle dark mode">β Theme</button><button class="btn" data-action="json">β JSON backup</button><button class="btn" data-action="import">β Restore</button><button class="btn primary" data-action="csv">β Export CSV</button></div></div><div id="notice" class="notice" role="status" aria-live="polite"></div><div id="pages"></div><div class="footnote">Hugdrigo Β· ScholarGraph Β· Project #111 Β· Fictional academic network Β· Local browser application Β· Β© 2026</div></main></div><div class="modalShade" id="modal" role="dialog" aria-modal="true" aria-label="Edit research network"><div class="modal" id="modalInner"></div></div><input type="file" id="importFile" class="hiddenFile" accept=".json,application/json" aria-label="Import JSON backup"> |
| <script> |
| 'use strict'; |
| (()=>{ |
| const KEY='hugdrigo_scholargraph_111_v1'; |
| const fields=['AI & Language','Public Policy','Sustainability','Health Systems']; |
| const fieldColors={'AI & Language':'#20948d','Public Policy':'#6d91c5','Sustainability':'#a0ba70','Health Systems':'#dc9e72'}; |
| const seedNodes=[ |
| {id:'r1',name:'Amelia Chen',field:fields[0],institution:'Northern Research Institute',country:'Canada',papers:32,citations:660,active:true}, |
| {id:'r2',name:'Matteo Rossi',field:fields[0],institution:'Meridian University',country:'Italy',papers:27,citations:522,active:true}, |
| {id:'r3',name:'Sofia Alvarez',field:fields[1],institution:'Andean Policy Lab',country:'Argentina',papers:19,citations:290,active:true}, |
| {id:'r4',name:'Nadia Okafor',field:fields[1],institution:'Global Civic Institute',country:'Nigeria',papers:41,citations:813,active:true}, |
| {id:'r5',name:'Oliver Brown',field:fields[2],institution:'Pacific Climate Centre',country:'Australia',papers:35,citations:740,active:true}, |
| {id:'r6',name:'Isabella Costa',field:fields[2],institution:'Atlantic Science Hub',country:'Brazil',papers:22,citations:365,active:true}, |
| {id:'r7',name:'Priya Shah',field:fields[3],institution:'Health Futures Center',country:'India',papers:37,citations:790,active:true}, |
| {id:'r8',name:'Noah Kim',field:fields[3],institution:'Global Biomedical Network',country:'South Korea',papers:24,citations:455,active:true}, |
| {id:'r9',name:'Lucia Ferraro',field:fields[0],institution:'Meridian University',country:'Italy',papers:18,citations:242,active:true}, |
| {id:'r10',name:'Daniel Mensah',field:fields[1],institution:'Global Civic Institute',country:'Ghana',papers:23,citations:360,active:true}, |
| {id:'r11',name:'Elena Novak',field:fields[2],institution:'Central Ecology Institute',country:'Czechia',papers:21,citations:312,active:true}, |
| {id:'r12',name:'Samuel Reed',field:fields[3],institution:'Health Futures Center',country:'United States',papers:16,citations:231,active:true} |
| ]; |
| const seedLinks=[['r1','r2',4],['r1','r9',3],['r2','r9',2],['r3','r4',3],['r3','r10',2],['r4','r10',5],['r5','r6',4],['r5','r11',3],['r6','r11',2],['r7','r8',4],['r7','r12',2],['r8','r12',3],['r2','r3',2],['r4','r5',2],['r6','r7',1]].map(([a,b,projects],i)=>({id:'e'+(i+1),a,b,projects,active:true})); |
| const defaults=()=>({version:1,nodes:structuredClone(seedNodes),links:structuredClone(seedLinks),scenario:'baseline',removal:12,simseed:1107,theme:'light',selected:'r2'}); |
| const validName=s=>typeof s==='string'&&s.trim().length>0&&s.length<=90; |
| const text=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); |
| const num=(v,lo,hi)=>typeof v==='number'&&Number.isFinite(v)&&v>=lo&&v<=hi; |
| const validate=raw=>{ |
| if(!raw||raw.version!==1||!Array.isArray(raw.nodes)||!Array.isArray(raw.links)||raw.nodes.length>40||raw.links.length>150)throw Error('Invalid file format or data limits'); |
| const ids=new Set();const pairs=new Set(); |
| for(const n of raw.nodes){if(!validName(n.id)||!validName(n.name)||!validName(n.institution)||!validName(n.country)||!fields.includes(n.field)||!num(n.papers,0,100000)||!Number.isInteger(n.papers)||!num(n.citations,0,10000000)||!Number.isInteger(n.citations)||typeof n.active!=='boolean'||ids.has(n.id))throw Error('Invalid researcher data');ids.add(n.id)} |
| for(const e of raw.links){if(!validName(e.id)||!ids.has(e.a)||!ids.has(e.b)||e.a===e.b||!num(e.projects,1,500)||!Number.isInteger(e.projects)||typeof e.active!=='boolean')throw Error('Invalid collaboration data');const k=[e.a,e.b].sort().join('|');if(pairs.has(k))throw Error('Duplicate collaboration');pairs.add(k)} |
| return true; |
| }; |
| let state=defaults();try{const saved=JSON.parse(localStorage.getItem(KEY));validate(saved);state={...defaults(),...saved,scenario:typeof saved.scenario==='string'?saved.scenario:'baseline',removal:num(saved.removal,0,70)?saved.removal:12,simseed:num(saved.simseed,1,999999)?saved.simseed:1107,theme:saved.theme==='dark'?'dark':'light'}}catch(e){} |
| let tab='overview',filter='',edit='',search='';let toastTimer; |
| const persist=()=>{try{localStorage.setItem(KEY,JSON.stringify(state))}catch(e){notice('Browser storage is unavailable. Use JSON backups instead.')}}; |
| const notice=s=>{const el=document.getElementById('notice');el.textContent=s;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('show'),4200)}; |
| const live=()=>state.nodes.filter(n=>n.active); |
| const edges=()=>{const ids=new Set(live().map(n=>n.id));return state.links.filter(e=>e.active&&ids.has(e.a)&&ids.has(e.b))}; |
| function analyze(ns=live(),es=edges()){ |
| const ids=ns.map(n=>n.id),adj=new Map(ids.map(id=>[id,new Set()]));const deg=Object.fromEntries(ids.map(id=>[id,0])); |
| for(const e of es){if(!adj.has(e.a)||!adj.has(e.b)||e.a===e.b)continue;adj.get(e.a).add(e.b);adj.get(e.b).add(e.a)} |
| for(const id of ids)deg[id]=adj.get(id).size; |
| const visited=new Set(),components=[]; |
| for(const id of ids){if(visited.has(id))continue;const c=[],queue=[id];visited.add(id);for(let q=0;q<queue.length;q++){const v=queue[q];c.push(v);for(const w of adj.get(v))if(!visited.has(w)){visited.add(w);queue.push(w)}}components.push(c)} |
| const between=Object.fromEntries(ids.map(id=>[id,0])); |
| for(const s of ids){const stack=[],pred=Object.fromEntries(ids.map(v=>[v,[]])),sigma=Object.fromEntries(ids.map(v=>[v,0])),dist=Object.fromEntries(ids.map(v=>[v,-1]));sigma[s]=1;dist[s]=0;const Q=[s];for(let z=0;z<Q.length;z++){const v=Q[z];stack.push(v);for(const w of adj.get(v)){if(dist[w]<0){dist[w]=dist[v]+1;Q.push(w)}if(dist[w]===dist[v]+1){sigma[w]+=sigma[v];pred[w].push(v)}}}const delta=Object.fromEntries(ids.map(v=>[v,0]));while(stack.length){const w=stack.pop();for(const v of pred[w])delta[v]+=(sigma[v]/sigma[w])*(1+delta[w]);if(w!==s)between[w]+=delta[w]}} |
| for(const id of ids)between[id]/=2; |
| const discovery={},low={},parent={},bridges=[],visitedDFS=new Set();let tick=0;function dfs(u){visitedDFS.add(u);discovery[u]=low[u]=++tick;for(const v of adj.get(u)){if(!visitedDFS.has(v)){parent[v]=u;dfs(v);low[u]=Math.min(low[u],low[v]);if(low[v]>discovery[u])bridges.push([u,v].sort().join('|'))}else if(v!==parent[u])low[u]=Math.min(low[u],discovery[v])}}for(const id of ids)if(!visitedDFS.has(id))dfs(id); |
| return {nodes:ns,links:es,components,degree:deg,betweenness:between,bridges,density:ids.length>1?es.length/(ids.length*(ids.length-1)/2):0,largest:Math.max(0,...components.map(c=>c.length)),institutions:new Set(ns.map(n=>n.institution)).size}; |
| } |
| const percent=v=>(v*100).toFixed(1)+'%';const fmt=v=>Number(v).toLocaleString('en-US',{maximumFractionDigits:0}); |
| function rng(seed){let s=(Math.floor(seed)>>>0)||1;return ()=>{s^=s<<13;s^=s>>>17;s^=s<<5;return (s>>>0)/4294967296}} |
| function simulate(ns,es,loss,seed,iterations=360){let r=rng(seed),sizeSum=0,compSum=0,isolatedSum=0;for(let k=0;k<iterations;k++){const keep=es.filter(()=>r()*100>=loss);const a=analyze(ns,keep);sizeSum+=ns.length?a.largest/ns.length:0;compSum+=a.components.length;isolatedSum+=a.components.filter(c=>c.length===1).length}return {resilience:sizeSum/iterations,components:compSum/iterations,isolated:isolatedSum/iterations}} |
| const scenarios={baseline:{title:'Current network',impact:0,loss:12},conference:{title:'Academic conference',impact:2,loss:8},partnership:{title:'Cross-sector partnership',impact:3,loss:10},disruption:{title:'Collaboration disruption',impact:0,loss:35}}; |
| function proposals(a){const ns=a.nodes,existing=new Set(a.links.map(e=>[e.a,e.b].sort().join('|'))),groups=Object.fromEntries(a.components.flatMap((c,i)=>c.map(id=>[id,i]))),items=[]; |
| for(let i=0;i<ns.length;i++)for(let j=i+1;j<ns.length;j++){const x=ns[i],y=ns[j],key=[x.id,y.id].sort().join('|');if(existing.has(key))continue;let score=10+(groups[x.id]!==groups[y.id]?45:0)+(x.field!==y.field?22:0)+(x.institution!==y.institution?12:0)+(x.country!==y.country?8:0)-Math.abs(a.degree[x.id]-a.degree[y.id]);items.push({a:x.id,b:y.id,score,bridge:groups[x.id]!==groups[y.id],diverse:x.field!==y.field})}return items.sort((x,y)=>y.score-x.score||[x.a,x.b].join().localeCompare([y.a,y.b].join()))} |
| const getName=id=>state.nodes.find(n=>n.id===id)?.name||id; |
| const topNodes=a=>[...a.nodes].sort((x,y)=>a.betweenness[y.id]-a.betweenness[x.id]||a.degree[y.id]-a.degree[x.id]); |
| function layout(a){const ns=a.nodes,sz=ns.length,group={};fields.forEach((f,i)=>group[f]=i);const locations={};if(!sz)return locations; |
| const area=[150,610],anchors=[[250,95],[535,95],[250,295],[535,295]]; |
| fields.forEach(f=>{const members=ns.filter(n=>n.field===f),anchor=anchors[group[f]];members.forEach((n,i)=>{let ang=(i/members.length)*Math.PI*2+group[f]*.25;locations[n.id]={x:anchor[0]+(members.length>1?90:0)*Math.cos(ang),y:anchor[1]+(members.length>1?56:0)*Math.sin(ang)}})}); |
| // A deterministic, bounded force simulation keeps clusters legible even after editing. |
| for(let k=0;k<90;k++){const move=Object.fromEntries(ns.map(n=>[n.id,{x:0,y:0}]));for(let i=0;i<ns.length;i++)for(let j=i+1;j<ns.length;j++){const ai=ns[i].id,bj=ns[j].id,p=locations[ai],q=locations[bj],dx=p.x-q.x,dy=p.y-q.y,d2=Math.max(120,(dx*dx+dy*dy)),force=7000/d2,dist=Math.sqrt(d2);move[ai].x+=dx/dist*force;move[ai].y+=dy/dist*force;move[bj].x-=dx/dist*force;move[bj].y-=dy/dist*force} |
| for(const e of a.links){const p=locations[e.a],q=locations[e.b];const dx=q.x-p.x,dy=q.y-p.y,d=Math.max(1,Math.hypot(dx,dy)),pull=.012*(d-(e.projects>2?155:190));move[e.a].x+=dx/d*pull;move[e.a].y+=dy/d*pull;move[e.b].x-=dx/d*pull;move[e.b].y-=dy/d*pull} |
| for(const n of ns){let p=locations[n.id],m=move[n.id];p.x=Math.max(37,Math.min(743,p.x+m.x*.3));p.y=Math.max(37,Math.min(355,p.y+m.y*.3))}} |
| return locations} |
| const svgGrid=()=>'<defs><pattern id="gridpoints" width="30" height="30" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r=".8" fill="#b8d6d4" opacity=".6"/></pattern></defs><rect width="780" height="390" fill="url(#gridpoints)"/>'; |
| function networkSVG(a){const positions=layout(a),bridges=new Set(a.bridges),s=state.selected;return `<svg viewBox="0 0 780 390" role="img" aria-label="Research collaboration network of ${a.nodes.length} fictional scholars and ${a.links.length} collaboration links">${svgGrid()}${a.links.map(e=>{let p=positions[e.a],q=positions[e.b],bridge=bridges.has([e.a,e.b].sort().join('|'));return `<line x1="${p.x}" y1="${p.y}" x2="${q.x}" y2="${q.y}" stroke="${bridge?'#e1a263':'#8cb9b9'}" stroke-width="${Math.min(5,1.2+e.projects*.55)}" stroke-opacity=".64"/>`}).join('')}${a.nodes.map(n=>{const p=positions[n.id],selected=n.id===s;return `<g class="node" data-select="${text(n.id)}" tabindex="0" role="button" aria-label="Select ${text(n.name)}"><circle cx="${p.x}" cy="${p.y}" r="${selected?22:18}" fill="${fieldColors[n.field]}" stroke="${selected?'#154e4c':'#fff'}" stroke-width="${selected?'4':'3'}"/><text x="${p.x}" y="${p.y+4}" text-anchor="middle" fill="white" font-size="11" font-weight="800" pointer-events="none">${text(n.name.split(' ').map(x=>x[0]).slice(0,2).join(''))}</text><text x="${p.x}" y="${p.y+34}" text-anchor="middle" font-size="10" font-weight="700" fill="var(--ink)" pointer-events="none">${text(n.name.split(' ').at(-1))}</text></g>`}).join('')}</svg>`} |
| function selectedCard(a){const n=state.nodes.find(x=>x.id===state.selected&&x.active)||a.nodes[0];if(!n)return '<div class="empty">No active researchers</div>';return `<div style="display:flex;gap:12px;align-items:center;margin-bottom:13px"><div class="avatar" style="width:46px;height:46px;font-size:15px;background:${fieldColors[n.field]}">${text(n.name.split(' ').map(v=>v[0]).join(''))}</div><div><b style="font-size:15px">${text(n.name)}</b><div class="ranksub">${text(n.institution)} Β· ${text(n.country)}</div></div></div><div class="two"><div class="stat"><small>Direct collaborators</small><strong>${a.degree[n.id]||0}</strong></div><div class="stat"><small>Bridge centrality</small><strong>${(a.betweenness[n.id]||0).toFixed(1)}</strong></div></div><p style="font-size:11px;margin-top:11px"><span class="tag">${text(n.field)}</span> ${fmt(n.papers)} publications Β· ${fmt(n.citations)} example citations</p><p style="font-size:10px">Centrality uses unweighted shortest paths; all academic metrics are synthetic.</p>`} |
| const pieChart=a=>{const groups=fields.map(f=>({f,n:a.nodes.filter(n=>n.field===f).length})),tot=Math.max(1,a.nodes.length);let start=0;const rays=groups.map(g=>{const angle=g.n/tot*100;const x=start;start+=angle;return `${fieldColors[g.f]} ${x}% ${start}%`}).join(',');return `<div style="display:flex;align-items:center;gap:22px"><div style="width:145px;height:145px;flex-shrink:0;border-radius:50%;background:conic-gradient(${rays});display:grid;place-items:center"><div style="background:var(--card);width:95px;height:95px;border-radius:50%;display:grid;place-items:center;text-align:center"><span><b style="font-size:27px">${a.nodes.length}</b><br><small style="color:var(--muted)">scholars</small></span></div></div><div style="flex:1">${groups.map(g=>`<div class="miniRow"><span style="display:flex;align-items:center;gap:6px"><i style="width:8px;height:8px;background:${fieldColors[g.f]};border-radius:50%"></i>${text(g.f)}</span><strong>${g.n}</strong></div>`).join('')}</div></div>`} |
| function kpi(title,value,sub,ico){return `<div class="kpi"><div class="kpiTop"><span>${title}</span><span class="kpiIcon">${ico}</span></div><strong>${value}</strong><p>${sub}</p></div>`} |
| function header(kicker,title,sub){return `<div class="header"><div><div class="eyebrow">${kicker}</div><h1>${title}</h1><p class="subtitle">${sub}</p></div><span class="pill">β Synthetic demonstration</span></div>`} |
| function overview(){const a=analyze(),leaders=topNodes(a).slice(0,5),props=proposals(a),connectivity=a.nodes.length?(a.largest/a.nodes.length):0;return `${header('Research network intelligence','Collaboration, mapped with clarity.','Explore research connections, discover institutional bridges and examine network robustness with transparent graph algorithms.')} |
| <div class="kpis">${kpi('Active researchers',a.nodes.length,'Across '+a.institutions+' fictional institutions','β')}${kpi('Collaborations',a.links.length,'Undirected co-research connections','β')}${kpi('Network density',percent(a.density),'Share of possible active links','β')}${kpi('Connected groups',a.components.length,'Largest group '+percent(connectivity),'β')}</div> |
| <div class="grid"><section class="panel"><div class="panelHead"><div><h2>Research collaboration map</h2><p>Click a researcher to inspect their connections</p></div><span class="labelTag">${a.bridges.length} critical links</span></div><div class="network">${networkSVG(a)}<div class="networkTip">β Nodes are researchers β Lines represent collaborations</div></div><div class="legend">${fields.map(f=>`<span><i style="background:${fieldColors[f]}"></i>${f}</span>`).join('')}<span><i style="background:#e1a263"></i>Bridge link</span></div></section><div class="stack"><section class="panel"><div class="panelHead"><div><h2>Researcher snapshot</h2><p>Selected network node</p></div></div>${selectedCard(a)}</section><section class="panel"><div class="panelHead"><div><h2>Network intelligence</h2><p>Structure at a glance</p></div></div><div class="miniRow"><span>Critical bridge links</span><strong>${a.bridges.length}</strong></div><div class="miniRow"><span>Cross-discipline connections</span><strong>${a.links.filter(e=>a.nodes.find(n=>n.id===e.a)?.field!==a.nodes.find(n=>n.id===e.b)?.field).length}</strong></div><div class="miniRow"><span>Isolated researchers</span><strong>${a.components.filter(c=>c.length===1).length}</strong></div><div class="miniRow"><span>Opportunity suggestions</span><strong>${props.length}</strong></div></section></div></div> |
| <div class="grid equal"><section class="panel"><div class="panelHead"><div><h2>Key network connectors</h2><p>Ranked by shortest-path betweenness centrality</p></div><button class="btn mini" data-nav="network">Explore network β</button></div><div class="ranking">${leaders.map((n,i)=>`<div class="rank"><div class="rankno">${i+1}</div><div class="avatar" style="background:${fieldColors[n.field]}">${text(n.name.split(' ').map(p=>p[0]).join(''))}</div><div style="flex:1;min-width:0"><div class="rankname">${text(n.name)}</div><div class="ranksub">${text(n.field)}</div></div><div class="rankmetric"><b>${a.betweenness[n.id].toFixed(1)}</b><small>bridge score</small></div></div>`).join('')}</div></section><section class="panel"><div class="panelHead"><div><h2>Research areas</h2><p>Active scholarly participation by field</p></div></div>${pieChart(a)}<div class="hint" style="margin-top:15px"><b>Next collaboration opportunity:</b> ${props[0]?`${text(getName(props[0].a))} + ${text(getName(props[0].b))}`:'Add researchers to discover matches'}. The heuristic prioritizes disconnected groups, distinct research fields and institutions.</div></section></div>`} |
| function researchNetwork(){const a=analyze(),ps=proposals(a);return `${header('Graph laboratory','Understand every connection.','Inspect the network structure, model potential bridges and explain why specific collaborations matter.')} |
| <div class="grid"><section class="panel"><div class="panelHead"><div><h2>Interactive graph</h2><p>${a.nodes.length} active nodes Β· ${a.links.length} active links</p></div><button class="btn mini" data-action="addlink">+ Collaboration</button></div><div class="network">${networkSVG(a)}<div class="networkTip">Select any node for details</div></div><div class="legend">${fields.map(f=>`<span><i style="background:${fieldColors[f]}"></i>${f}</span>`).join('')}</div></section><section class="panel"><div class="panelHead"><div><h2>Selected researcher</h2><p>Researcher-level centrality metrics</p></div></div>${selectedCard(a)}<div class="miniRow"><span>Connected groups</span><strong>${a.components.length}</strong></div><div class="miniRow"><span>Density</span><strong>${percent(a.density)}</strong></div><div class="hint" style="margin-top:17px"><b>How to read:</b> A bridge link disconnects part of a network when removed. Betweenness counts how often a node lies on shortest paths, independent of citations.</div></section></div> |
| <section class="panel"><div class="panelHead"><div><h2>Recommended new collaborations</h2><p>Transparent diversity and connectivity heuristic, not a personalized prediction</p></div><button class="btn mini" data-action="addlink">Add your own β</button></div><div class="tableWrap"><table><thead><tr><th>Researcher A</th><th>Researcher B</th><th>Suggested benefit</th><th>Priority score</th><th>Action</th></tr></thead><tbody>${ps.slice(0,8).map(p=>`<tr><td><b>${text(getName(p.a))}</b></td><td><b>${text(getName(p.b))}</b></td><td>${p.bridge?'Connect separate groups':p.diverse?'Cross-disciplinary':'Extend collaboration'}</td><td><b>${p.score}</b> / 100+</td><td><button class="btn mini" data-pair="${text(p.a)}|${text(p.b)}">Connect</button></td></tr>`).join('')||'<tr><td colspan="5">No available proposals</td></tr>'}</tbody></table></div></section>`} |
| function simulatePage(){const a=analyze(),options=Object.entries(scenarios).map(([id,v])=>`<option value="${id}" ${state.scenario===id?'selected':''}>${v.title}</option>`).join(''),s=scenarios[state.scenario]||scenarios.baseline,ps=proposals(a),added=ps.slice(0,s.impact).map((p,i)=>({id:'virtual'+i,a:p.a,b:p.b,projects:1,active:true})),proj=analyze(a.nodes,[...a.links,...added]),base=simulate(a.nodes,a.links,s.loss,state.simseed),projection=simulate(proj.nodes,proj.links,s.loss,state.simseed);const bars=[{name:'Current resilience',v:base.resilience,c:'#a6bfc5'},{name:'Scenario resilience',v:projection.resilience,c:'#1a9a88'}];return `${header('Scenario studio','Test the strength of your network.','Run seeded disruption experiments and evaluate the effects of new research collaborations on network connectivity.')} |
| <div class="panel" style="margin-bottom:15px"><div class="panelHead"><div><h2>Scenario controls</h2><p>Results use repeated independent link removals, not forecasts of real institutions</p></div><span class="labelTag">360 seeded iterations</span></div><div class="controls"><div class="field"><label for="scenario">Scenario</label><select class="select" id="scenario">${options}</select></div><div class="field"><label for="loss">Per-link disruption probability</label><select class="select" id="loss">${[0,5,10,15,20,25,35,45,55,70].map(v=>`<option value="${v}" ${state.removal===v?'selected':''}>${v}%</option>`).join('')}</select></div><div class="field"><label for="simseed">Random seed (repeatable)</label><input class="input" type="number" min="1" max="999999" id="simseed" value="${state.simseed}"></div><button class="btn primary" data-action="run">β» Run simulation</button></div><p style="font-size:11px">Chosen scenario adds <b>${added.length}</b> suggested links to the network. Disruption rate uses the selected probability when set; scenario defaults are shown in methodology.</p></div> |
| <div class="kpis">${kpi('Projected density',percent(proj.density),'Current '+percent(a.density),'β')}${kpi('Projected bridge links',proj.bridges.length,'Current '+a.bridges.length,'β')}${kpi('Expected largest group',percent(projection.resilience),'Current '+percent(base.resilience),'β')}${kpi('Expected isolated nodes',projection.isolated.toFixed(1),'Current '+base.isolated.toFixed(1),'β')}</div> |
| <div class="grid equal"><section class="panel"><div class="panelHead"><div><h2>Resilience comparison</h2><p>Average retained share in the largest connected component</p></div></div><div style="display:flex;flex-direction:column;gap:24px;padding:12px 0">${bars.map(b=>`<div><div class="miniRow"><b>${b.name}</b><strong>${percent(b.v)}</strong></div><div style="height:19px;border-radius:20px;background:#e6eff1;overflow:hidden"><div style="width:${Math.max(0,Math.min(100,b.v*100))}%;height:100%;background:${b.c};border-radius:20px"></div></div></div>`).join('')}</div><div class="hint">A higher share means fewer researchers become separated after randomly removing collaboration links. Results are model outcomes, not measured resilience.</div></section><section class="panel"><div class="panelHead"><div><h2>Before vs. after</h2><p>Deterministic graph structure before random disruptions</p></div></div><div class="statline"><div class="stat"><small>Components</small><strong>${a.components.length} β ${proj.components.length}</strong></div><div class="stat"><small>Links</small><strong>${a.links.length} β ${proj.links.length}</strong></div><div class="stat"><small>Bridge nodes</small><strong>${topNodes(proj).filter(n=>proj.betweenness[n.id]>0).length}</strong></div></div><div class="timeline">${['Network','Scenario','Disruption'].map((t,i)=>`<div class="step ${i===2?'good':''}">${t}<b>${i===0?a.nodes.length:i===1?added.length:360}</b></div>`).join('')}</div><p style="margin-top:16px;font-size:11px">Potential collaborations added: ${added.length?added.map(x=>text(getName(x.a)+' + '+getName(x.b))).join('; '):'none'}</p></section></div>`} |
| function roster(){const ns=state.nodes.filter(n=>(!filter||n.field===filter)&&(!search||[n.name,n.institution,n.country].join(' ').toLowerCase().includes(search)));const a=analyze();return `${header('Scholar directory','Curate your research network.','Add researchers, update institutional details and archive records without permanently deleting them.')} |
| <div class="panel"><div class="panelHead"><div><h2>Researcher registry</h2><p>${live().length} active of ${state.nodes.length} total records</p></div><button class="btn primary" data-action="addnode">+ Add researcher</button></div><div class="controls"><div class="field"><label for="search">Search</label><input id="search" class="input" placeholder="Name, institution, country" value="${text(search)}"></div><div class="field"><label for="filter">Research field</label><select id="filter" class="select"><option value="">All fields</option>${fields.map(f=>`<option ${filter===f?'selected':''}>${f}</option>`).join('')}</select></div></div><div class="tableWrap"><table><thead><tr><th>Researcher</th><th>Research field</th><th>Institution / Country</th><th>Collaborators</th><th>Status</th><th>Actions</th></tr></thead><tbody>${ns.map(n=>`<tr class="${n.active?'':'inactive'}"><td><b>${text(n.name)}</b><div class="ranksub">${fmt(n.papers)} publications Β· ${fmt(n.citations)} citations</div></td><td>${text(n.field)}</td><td>${text(n.institution)}<div class="ranksub">${text(n.country)}</div></td><td>${a.degree[n.id]||0}</td><td><span class="tag ${n.active?'':'archived'}">${n.active?'Active':'Archived'}</span></td><td><div class="actioncell"><button class="btn mini" data-edit="${text(n.id)}">Edit</button><button class="btn mini" data-toggle="${text(n.id)}">${n.active?'Archive':'Restore'}</button></div></td></tr>`).join('')||'<tr><td colspan="6" class="empty">No matching researchers</td></tr>'}</tbody></table></div><p style="font-size:10px">Archiving a researcher also excludes their collaborations from active network calculations. All records remain in local storage.</p></div>`} |
| function collaborations(){const a=analyze();return `${header('Relationship ledger','Manage collaborations.','Adjust project counts, archive outdated connections and create new links between active researchers.')} |
| <div class="kpis">${kpi('Links on record',state.links.length,'Archived relationships included','β')}${kpi('Active links',a.links.length,'Across active researchers','β')}${kpi('Joint projects',a.links.reduce((s,e)=>s+e.projects,0),'Synthetic project counts','β')}${kpi('Bridge links',a.bridges.length,'Single-point network connections','β')}</div><section class="panel"><div class="panelHead"><div><h2>Collaboration register</h2><p>Undirected network relationships</p></div><button class="btn primary" data-action="addlink">+ Add collaboration</button></div><div class="tableWrap"><table><thead><tr><th>Researcher A</th><th>Researcher B</th><th>Shared projects</th><th>Structure</th><th>Status</th><th>Actions</th></tr></thead><tbody>${state.links.map(e=>{const active=a.links.some(x=>x.id===e.id),bridge=active&&a.bridges.includes([e.a,e.b].sort().join('|'));return `<tr class="${active?'':'inactive'}"><td><b>${text(getName(e.a))}</b></td><td><b>${text(getName(e.b))}</b></td><td>${e.projects}</td><td>${bridge?'<span class="tag bridge">Critical bridge</span>':'Standard link'}</td><td><span class="tag ${e.active?'':'archived'}">${e.active?'Recorded':'Archived'}</span></td><td><div class="actioncell"><button class="btn mini" data-editlink="${text(e.id)}">Edit</button><button class="btn mini" data-togglelink="${text(e.id)}">${e.active?'Archive':'Restore'}</button></div></td></tr>`}).join('')}</tbody></table></div></section>`} |
| function methodology(){return `${header('Transparent methodology','Understand the model.','No black-box machine learning, private data collection or external model calls. All outputs come from explained graph algorithms and synthetic examples.')} |
| <div class="grid equal"><section class="panel"><h2>What ScholarGraph calculates</h2><div class="textBlock"><ul class="methodList"><li><b>Degree centrality:</b> the number of a scholar's unique active collaborators.</li><li><b>Betweenness centrality:</b> the number of shortest paths passing through each researcher (Brandes algorithm, undirected and unweighted).</li><li><b>Connected components:</b> groups of researchers linked through collaboration paths.</li><li><b>Network density:</b> active edges divided by all possible unique pairings, <code>m / (n Γ (n - 1) / 2)</code>.</li><li><b>Bridge links:</b> relationships whose removal increases the number of components, found using low-link DFS.</li><li><b>Collaboration suggestions:</b> a transparent score giving preference to distinct disconnected groups, fields, institutions and countries. Not a machine-learning recommendation.</li></ul></div></section><section class="panel"><h2>Simulation rules and limitations</h2><div class="textBlock"><ul class="methodList"><li><b>Disruptions:</b> each active link is independently removed with the configured probability on each trial.</li><li><b>Resilience metric:</b> average proportion of active researchers in the largest connected group across <b>360</b> seeded iterations.</li><li><b>Scenario additions:</b> the highest-ranked proposed links are added virtually; they are not saved until you select <b>Connect</b>.</li><li><b>Determinism:</b> using the same network, seed and inputs produces the same result.</li><li><b>No real researchers:</b> institutions, articles, citation numbers and collaborations are synthetic placeholders.</li><li><b>Not bibliometrics:</b> citation counts are illustrative only and do not affect the core network algorithms.</li></ul></div><div class="hint"><b>Privacy:</b> changes are saved in your browser when localStorage is permitted. JSON backups can be exported. No analytics trackers, external services or packages are loaded.</div></section></div><div class="panel"><div class="panelHead"><div><h2>Data lifecycle</h2><p>Fully portable, editable research network demonstration</p></div></div><div class="thirds"><div class="stat"><small>Data entry</small><strong style="font-size:17px">Local forms</strong><p style="font-size:11px">Add and edit researchers and collaborations.</p></div><div class="stat"><small>Portable copies</small><strong style="font-size:17px">CSV + JSON</strong><p style="font-size:11px">Export summaries and import backups.</p></div><div class="stat"><small>Security bounds</small><strong style="font-size:17px">Input validation</strong><p style="font-size:11px">Reject invalid IDs, duplicates and excessive data sizes.</p></div></div></div>`} |
| const pages={overview,network:researchNetwork,scenarios:simulatePage,researchers:roster,links:collaborations,methodology}; |
| const menus=[['overview','β«','Overview'],['network','β','Collaboration map'],['scenarios','β¦','Scenario lab'],['researchers','β€','Researcher registry'],['links','β','Collaboration ledger'],['methodology','⬑','Methodology']]; |
| function render(){document.body.classList.toggle('dark',state.theme==='dark');document.getElementById('nav').innerHTML=menus.map(([id,s,l])=>`<button data-nav="${id}" class="${tab===id?'active':''}"><span class="symbol">${s}</span>${l}</button>`).join('');document.getElementById('mobileNav').innerHTML='<span class="brandmini">β£ ScholarGraph</span>'+menus.map(([id,s,l])=>`<button data-nav="${id}" class="${tab===id?'active':''}">${s} ${l.replace('Collaboration ','').replace('Researcher ','')}</button>`).join('');document.getElementById('pages').innerHTML=`<div class="page active" id="currentPage">${pages[tab]()}</div>`;persist()} |
| const closeModal=()=>{document.getElementById('modal').classList.remove('show');document.getElementById('modalInner').innerHTML=''}; |
| function openNode(id=''){const n=state.nodes.find(n=>n.id===id)||{id:'',name:'',field:fields[0],institution:'',country:'',papers:0,citations:0,active:true};edit=id;document.getElementById('modalInner').innerHTML=`<div class="modalHeader"><h2>${id?'Edit':'Add'} researcher</h2><button class="btn mini" data-action="close" aria-label="Close dialog">β</button></div><form id="nodeForm"><div class="formGrid"><div class="field full"><label for="nf_name">Full name</label><input class="input" id="nf_name" maxlength="90" required value="${text(n.name)}"></div><div class="field"><label for="nf_field">Research field</label><select class="select" id="nf_field">${fields.map(f=>`<option ${n.field===f?'selected':''}>${f}</option>`).join('')}</select></div><div class="field"><label for="nf_country">Country</label><input class="input" id="nf_country" required maxlength="90" value="${text(n.country)}"></div><div class="field full"><label for="nf_inst">Institution</label><input class="input" id="nf_inst" required maxlength="90" value="${text(n.institution)}"></div><div class="field"><label for="nf_papers">Example publications</label><input class="input" id="nf_papers" type="number" min="0" max="100000" step="1" required value="${n.papers}"></div><div class="field"><label for="nf_cites">Example citations</label><input class="input" id="nf_cites" type="number" min="0" max="10000000" step="1" required value="${n.citations}"></div></div><div class="modalActions"><button type="button" class="btn" data-action="close">Cancel</button><button class="btn primary" type="submit">Save researcher</button></div></form>`;document.getElementById('modal').classList.add('show');document.getElementById('nf_name').focus()} |
| function openLink(id='',a='',b=''){const e=state.links.find(e=>e.id===id)||{id:'',a,b,projects:1,active:true};edit=id;const opts=v=>live().map(n=>`<option value="${text(n.id)}" ${n.id===v?'selected':''}>${text(n.name)}</option>`).join('');document.getElementById('modalInner').innerHTML=`<div class="modalHeader"><h2>${id?'Edit':'Add'} collaboration</h2><button class="btn mini" data-action="close">β</button></div><form id="linkForm"><div class="formGrid"><div class="field full"><label for="lf_a">Researcher A</label><select class="select" id="lf_a" required>${opts(e.a)}</select></div><div class="field full"><label for="lf_b">Researcher B</label><select class="select" id="lf_b" required>${opts(e.b)}</select></div><div class="field full"><label for="lf_projects">Shared projects (illustrative)</label><input class="input" type="number" min="1" max="500" step="1" required id="lf_projects" value="${e.projects}"></div></div><div class="modalActions"><button type="button" class="btn" data-action="close">Cancel</button><button type="submit" class="btn primary">Save collaboration</button></div></form>`;document.getElementById('modal').classList.add('show')} |
| const fileSave=(name,body,mime)=>{const blob=new Blob([body],{type:mime}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000)}; |
| const csvValue=v=>{let s=String(v??'');if(/^[\s\uFEFF]*[=+\-@\t\r]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}; |
| function exportCSV(){const a=analyze(),rows=[['id','name','field','institution','country','papers_demo','citations_demo','active','degree','betweenness'],...state.nodes.map(n=>[n.id,n.name,n.field,n.institution,n.country,n.papers,n.citations,n.active?'yes':'no',a.degree[n.id]||0,(a.betweenness[n.id]||0).toFixed(3)])];fileSave('scholargraph_111_researchers.csv','\uFEFF'+rows.map(r=>r.map(csvValue).join(',')).join('\r\n'),'text/csv;charset=utf-8');notice('Researcher CSV downloaded. JSON backup includes collaborations.')} |
| function downloadJSON(){fileSave('scholargraph_111_backup.json',JSON.stringify(state,null,2),'application/json');notice('JSON backup downloaded.')} |
| function handleClick(e){let t=e.target.closest('[data-nav],[data-select],[data-pair],[data-edit],[data-toggle],[data-editlink],[data-togglelink],[data-action]');if(!t)return;if(t.dataset.nav){tab=t.dataset.nav;render();return}if(t.dataset.select){state.selected=t.dataset.select;render();return}if(t.dataset.pair){const [a,b]=t.dataset.pair.split('|');openLink('',a,b);return}if(t.dataset.edit){openNode(t.dataset.edit);return}if(t.dataset.toggle){const n=state.nodes.find(n=>n.id===t.dataset.toggle);if(n)n.active=!n.active;render();notice('Researcher status updated.');return}if(t.dataset.editlink){openLink(t.dataset.editlink);return}if(t.dataset.togglelink){const l=state.links.find(l=>l.id===t.dataset.togglelink);if(l)l.active=!l.active;render();notice('Collaboration status updated.');return} |
| const action=t.dataset.action; |
| if(action==='theme'){state.theme=state.theme==='dark'?'light':'dark';render()} |
| if(action==='json')downloadJSON();if(action==='csv')exportCSV();if(action==='import')document.getElementById('importFile').click();if(action==='addnode'){if(state.nodes.length>=40){notice('Maximum of 40 records reached');return}openNode()} |
| if(action==='addlink'){if(state.links.length>=150){notice('Maximum of 150 links reached');return}if(live().length<2){notice('Add two active researchers first');return}openLink()} |
| if(action==='run'){const sd=Number(document.getElementById('simseed').value);state.simseed=Math.round(Math.max(1,Math.min(999999,Number.isFinite(sd)?sd:1107)));state.removal=Number(document.getElementById('loss').value);state.scenario=document.getElementById('scenario').value;render();notice('Scenario recalculated with reproducible seed.')} |
| if(action==='close')closeModal()} |
| document.addEventListener('click',handleClick);document.addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&e.target.matches('[data-select]')){e.preventDefault();state.selected=e.target.dataset.select;render()}if(e.key==='Escape')closeModal()});document.getElementById('modal').addEventListener('click',e=>{if(e.target.id==='modal')closeModal()}); |
| document.addEventListener('submit',e=>{if(e.target.id==='nodeForm'){e.preventDefault();const n={id:edit||'r'+(Math.max(0,...state.nodes.map(n=>Number(n.id.replace(/\D/g,''))||0))+1),name:document.getElementById('nf_name').value.trim(),field:document.getElementById('nf_field').value,institution:document.getElementById('nf_inst').value.trim(),country:document.getElementById('nf_country').value.trim(),papers:Number(document.getElementById('nf_papers').value),citations:Number(document.getElementById('nf_cites').value),active:true};if(!validName(n.name)||!validName(n.country)||!validName(n.institution)||!Number.isInteger(n.papers)||!Number.isInteger(n.citations)||!num(n.papers,0,100000)||!num(n.citations,0,10000000)){notice('Enter valid researcher data');return}let idx=state.nodes.findIndex(x=>x.id===edit);if(idx>=0)state.nodes[idx]={...state.nodes[idx],...n};else state.nodes.push(n);state.selected=n.id;closeModal();render();notice('Researcher saved.');return} |
| if(e.target.id==='linkForm'){e.preventDefault();const a=document.getElementById('lf_a').value,b=document.getElementById('lf_b').value,p=Number(document.getElementById('lf_projects').value);if(a===b||!Number.isInteger(p)||!num(p,1,500)||state.links.some(l=>l.id!==edit&&[l.a,l.b].sort().join('|')===[a,b].sort().join('|'))){notice('Choose two distinct researchers with no existing link, and 1β500 shared projects.');return}const node={id:edit||'e'+(Math.max(0,...state.links.map(n=>Number(n.id.replace(/\D/g,''))||0))+1),a,b,projects:p,active:true};const idx=state.links.findIndex(l=>l.id===edit);if(idx>=0)state.links[idx]={...state.links[idx],...node};else state.links.push(node);closeModal();render();notice('Collaboration saved.')}}); |
| document.addEventListener('change',e=>{if(e.target.id==='filter'){filter=e.target.value;render()}if(e.target.id==='scenario'){state.scenario=e.target.value;state.removal=scenarios[state.scenario].loss;render()}if(e.target.id==='loss'){state.removal=Number(e.target.value);render()}});document.addEventListener('input',e=>{if(e.target.id==='search'){search=e.target.value;const pos=e.target.selectionStart;render();let s=document.getElementById('search');if(s){s.focus();s.setSelectionRange(pos,pos)}}}); |
| document.getElementById('importFile').addEventListener('change',async e=>{const f=e.target.files?.[0];if(!f)return;try{if(f.size>800000)throw Error('Backup is too large');const data=JSON.parse(await f.text());validate(data);state={...defaults(),...data,theme:data.theme==='dark'?'dark':'light',scenario:scenarios[data.scenario]?data.scenario:'baseline',removal:num(data.removal,0,70)?data.removal:12,simseed:num(data.simseed,1,999999)?data.simseed:1107};if(!state.nodes.some(n=>n.id===state.selected&&n.active))state.selected=live()[0]?.id||'';filter='';search='';render();notice('JSON restored successfully.')}catch(err){notice('Import rejected: '+err.message)}finally{e.target.value=''}}); |
| window.ScholarGraphQA={analyze,simulate,validate,rng,proposals,defaults,percent};render(); |
| })(); |
| </script> |
| </body> |
| </html> |
|
|