(function(){ // Safari-safe: no ?? and no mixed ?? with ||/&& var DATA = (window.PHI_DATA && typeof window.PHI_DATA === 'object') ? window.PHI_DATA : { nodes: [], links: [] }; var svg = d3.select('#graph'); var tooltip = d3.select('#tooltip'); var sidebar = document.getElementById('sidebar-content'); var searchInput = document.getElementById('search'); var width = svg.node().clientWidth || 1200; var height = svg.node().clientHeight || 800; var minR = 6, maxR = 24, ellipseAspect = 1.6; var satDot = 4.6, spiralStepR = 8, spiralStepTheta = 0.48*Math.PI, spiralStartR = 52; function esc(s){return String(s||'').replace(/[&<>"']/g,function(c){return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);});} function safeArray(x){return Array.isArray(x)?x:[];} function ageClass(d){ var a = d && d.ageDays; if (a == null) return 'age-old'; if (a <= 14) return 'age-very-new'; if (a <= 45) return 'age-new'; if (a <= 120) return 'age-mid'; if (a <= 270) return 'age-old'; return 'age-very-old'; } svg.attr('viewBox', '0 0 ' + width + ' ' + height); var root = svg.append('g'); var linkLayer = root.append('g').attr('class','links'); var nodeLayer = root.append('g').attr('class','nodes'); var satLayer = root.append('g').attr('class','satellites'); var zoom = d3.zoom().scaleExtent([0.35, 4]).on('zoom', function(ev){ root.attr('transform', ev.transform); }); svg.call(zoom); // clear selection when clicking background svg.on('click', function(ev){ if (ev.target === svg.node()){ clearSatellites(); clearFocus(); sidebar.innerHTML = ''; } }); // Build graph var idToNode = new Map(DATA.nodes.map(function(n){ return [n.id, n]; })); var links = DATA.links.map(function(l){ var s = idToNode.get(l.source) || l.source; var t = idToNode.get(l.target) || l.target; return { source:s, target:t }; }).filter(function(l){ return l.source && l.target; }); // degree as fallback for sizing var degree = new Map(); links.forEach(function(l){ degree.set(l.source.id, (degree.get(l.source.id)||0)+1); degree.set(l.target.id, (degree.get(l.target.id)||0)+1); }); var centralities = DATA.nodes.map(function(n){ return (typeof n.centrality === 'number') ? n.centrality : (degree.get(n.id)||0); }); var cMin = d3.min(centralities); if (cMin == null) cMin = 0; var cMax = d3.max(centralities); if (cMax == null) cMax = 1; var rScale = d3.scaleSqrt().domain([ (cMin||0.0001), (cMax||1) ]).range([minR, maxR]); var sim = d3.forceSimulation(DATA.nodes) .force('link', d3.forceLink(links).id(function(d){return d.id;}).distance(75).strength(0.7)) .force('charge', d3.forceManyBody().strength(-180)) .force('center', d3.forceCenter(width/2, height/2)) .force('collision', d3.forceCollide().radius(function(d){ var c = (typeof d.centrality === 'number') ? d.centrality : (degree.get(d.id)||1); return rScale(c) * 1.2; })); var linkSel = linkLayer.selectAll('line') .data(links) .join('line') .attr('class','link') .attr('stroke','#b9c7dd') .attr('stroke-opacity', .28); var nodeSel = nodeLayer.selectAll('g.node') .data(DATA.nodes, function(d){ return d.id; }) .join(function(enter){ var g = enter.append('g').attr('class','node'); g.append('ellipse') .attr('rx', function(d){ var c=(typeof d.centrality==='number')?d.centrality:(degree.get(d.id)||1); return rScale(c)*ellipseAspect; }) .attr('ry', function(d){ var c=(typeof d.centrality==='number')?d.centrality:(degree.get(d.id)||1); return rScale(c); }); g.append('text') .attr('x', function(d){ var c=(typeof d.centrality==='number')?d.centrality:(degree.get(d.id)||1); return rScale(c)*ellipseAspect + 4; }) .attr('y', 4) .text(function(d){ return d.id; }); g.on('mouseover', function(ev,d){ showTagTooltip(ev, d.id); }) .on('mousemove', moveTooltip) .on('mouseout', hideTooltip) .on('click', function(ev,d){ ev.stopPropagation(); onTagClick(d.id); }); return g; }); sim.on('tick', function(){ linkSel .attr('x1', function(d){return d.source.x;}) .attr('y1', function(d){return d.source.y;}) .attr('x2', function(d){return d.target.x;}) .attr('y2', function(d){return d.target.y;}); nodeSel.attr('transform', function(d){ return 'translate('+d.x+','+d.y+')'; }); // keep satellites stuck to hosts satLayer.selectAll('g.satellite') .attr('transform', function(d){ return 'translate('+(d.host.x + d._xoff)+','+(d.host.y + d._yoff)+')'; }); }); // Tooltip function showTagTooltip(evt, tag){ var desc = (DATA.tagDescriptions && DATA.tagDescriptions[tag]) ? DATA.tagDescriptions[tag] : '—'; var n = idToNode.get(tag); var deg = degree.get(tag) || 0; var cent = (n && typeof n.centrality==='number') ? n.centrality.toFixed(2) : String(deg||0); tooltip.html( '
'+esc(tag)+'
'+ '
degree '+deg+' • centrality '+cent+'
'+ '
'+esc(desc)+'
'+ '
Click to reveal pulse satellites
' ).style('display','block'); moveTooltip(evt); } function moveTooltip(evt){ var pad=12; tooltip.style('left',(evt.clientX+pad)+'px').style('top',(evt.clientY+pad)+'px'); } function hideTooltip(){ tooltip.style('display','none'); } // Focus / dim function setFocus(keepIds){ var keep = new Set(keepIds); nodeSel.classed('dim', function(d){ return !keep.has(d.id); }); linkSel.classed('dim', function(d){ return !(keep.has(d.source.id) && keep.has(d.target.id)); }); } function clearFocus(){ nodeSel.classed('dim', false); linkSel.classed('dim', false); } // Satellites (spiral) function clearSatellites(){ satLayer.selectAll('*').remove(); } function spiralOffsets(n){ var out=[], i; for(i=0;i'; return html; } function showPulseDetails(p){ var when = p.date ? ' ('+esc(p.date)+')' : ''; var html = '

'+esc(p.title || p.id || 'Pulse')+when+'

'; if (p.summary) html += '
'+esc(p.summary)+'
'; html += renderLinks('Papers', p.papers); html += renderLinks('Podcasts', p.podcasts); sidebar.innerHTML = html; } // Search function applyFilter(q){ var s = (q||'').trim().toLowerCase(); if (!s){ clearFocus(); return; } var keep = new Set(DATA.nodes.filter(function(n){ return n.id.toLowerCase().includes(s); }).map(function(n){ return n.id; })); setFocus(keep); } if (searchInput) searchInput.addEventListener('input', function(e){ applyFilter(e.target.value); }); // Start empty (no redundant helper text) sidebar.innerHTML = ''; })();