Qsearch / Templates /discovery.html
flyfir248's picture
Deployment v1.0 - Zero Secret History
d21c4c8
{% extends "base.html" %}
{% block title %}Expert Discovery | CPI Engine{% endblock %}
{% block content %}
<div class="min-h-screen bg-slate-50 p-8">
<div class="max-w-[1800px] mx-auto">
<!-- Header Section -->
<div class="mb-8">
<h1 class="text-4xl font-black text-slate-900 mb-2 uppercase italic flex items-center">
<i class="fas fa-users-cog text-indigo-600 mr-4"></i>Expert Discovery Engine
</h1>
<p class="text-slate-500 font-medium mb-2">A programmable scientific truth engine that ranks humans, not papers</p>
<p class="text-indigo-600 text-sm italic font-semibold">"Who demonstrably knows how to solve this problem?"</p>
<!-- Search Box -->
<form action="/discovery" method="GET" class="relative group mt-6">
<input type="text" name="q" value="{{ query }}"
placeholder="Enter research topic (e.g., 'T cell exhaustion in CAR-T therapy')..."
class="w-full bg-white border-2 border-slate-200 rounded-2xl px-8 py-5 text-lg shadow-sm focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 transition-all outline-none pr-48">
<button type="submit" class="absolute right-4 top-3 bottom-3 bg-indigo-600 text-white px-10 rounded-xl font-bold hover:bg-indigo-700 hover:shadow-lg transition-all active:scale-95">
Discover Experts
</button>
</form>
<!-- Status Bar -->
<div class="mt-4 flex items-center gap-6 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
OpenAlex Knowledge Graph
</div>
<div class="flex items-center gap-2">
<i class="fas fa-brain text-indigo-400"></i>
CPI Algorithm
</div>
{% if metadata and metadata.total_works %}
<div class="flex items-center gap-2">
<i class="fas fa-database text-emerald-400"></i>
{{ metadata.total_works }} Papers • {{ metadata.topics_discovered }} Topics
</div>
{% endif %}
</div>
</div>
<!-- Error Message -->
{% if error_message %}
<div class="bg-red-50 border-2 border-red-200 rounded-2xl p-6 mb-8">
<div class="flex items-center gap-3">
<i class="fas fa-exclamation-triangle text-red-500 text-xl"></i>
<p class="text-red-700 font-medium">{{ error_message }}</p>
</div>
</div>
{% endif %}
{% if experts %}
<!-- Dual Interface: Table + Graph -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- LEFT: Ranked Expert Table -->
<div class="lg:col-span-5 space-y-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest">Ranked Experts (CPI Score)</h2>
<span class="text-[10px] font-bold text-indigo-600 bg-indigo-50 px-3 py-1 rounded-full">Showing Top 50 of {{ all_experts|length if all_experts else experts|length }}</span>
</div>
<div class="space-y-3 max-h-[1200px] overflow-y-auto pr-2">
{% for expert in experts %}
<div class="bg-white rounded-xl border border-slate-200 p-5 hover:border-indigo-400 hover:shadow-md transition-all cursor-pointer group"
onclick="showExpertModal('{{ expert.openalex_id }}')">>
<div class="flex items-start justify-between mb-3">
<div class="flex-1">
<div class="flex items-center gap-3 mb-1">
<span class="text-lg font-black text-slate-900">{{ loop.index }}</span>
<h3 class="font-bold text-slate-900 group-hover:text-indigo-600 transition-colors">
{{ expert.name }}
</h3>
</div>
{% if expert.institutions %}
<p class="text-[10px] text-slate-500 italic">{{ expert.institutions[0] }}</p>
{% endif %}
</div>
<div class="text-right">
<div class="text-2xl font-black text-indigo-600">{{ expert.cpi_score }}</div>
<div class="text-[8px] text-slate-400 font-bold uppercase">CPI</div>
</div>
</div>
<!-- Mini Metrics -->
<div class="grid grid-cols-3 gap-2 text-center mb-3">
<div class="bg-slate-50 rounded-lg py-2">
<div class="text-sm font-black text-slate-900">{{ expert.h_index }}</div>
<div class="text-[8px] text-slate-400 font-bold uppercase">h-index</div>
</div>
<div class="bg-slate-50 rounded-lg py-2">
<div class="text-sm font-black text-slate-900">{{ expert.recent_papers }}</div>
<div class="text-[8px] text-slate-400 font-bold uppercase">Recent</div>
</div>
<div class="bg-slate-50 rounded-lg py-2">
<div class="text-sm font-black text-slate-900">{{ expert.citation_velocity }}%</div>
<div class="text-[8px] text-slate-400 font-bold uppercase">Velocity</div>
</div>
</div>
<!-- CPI Breakdown Bars -->
<div class="grid grid-cols-3 gap-1">
<div>
<div class="h-1 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-indigo-500" style="width: {{ expert.contribution_score }}%"></div>
</div>
<div class="text-[7px] text-slate-400 font-bold mt-1">C: {{ expert.contribution_score }}%</div>
</div>
<div>
<div class="h-1 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-emerald-500" style="width: {{ expert.proof_score }}%"></div>
</div>
<div class="text-[7px] text-slate-400 font-bold mt-1">P: {{ expert.proof_score }}%</div>
</div>
<div>
<div class="h-1 bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-amber-500" style="width: {{ expert.impact_score }}%"></div>
</div>
<div class="text-[7px] text-slate-400 font-bold mt-1">I: {{ expert.impact_score }}%</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- RIGHT: Interactive Knowledge Graph -->
<div class="lg:col-span-7">
<div class="bg-white rounded-2xl border border-slate-200 p-6 sticky top-8">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-1">Field Topology</h2>
<p class="text-[10px] text-slate-500 font-medium">Interactive network showing expert positioning</p>
</div>
<div class="flex gap-2 text-[9px] font-bold">
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-indigo-600"></div>
<span class="text-slate-500">Query</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-emerald-500"></div>
<span class="text-slate-500">Topics</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 rounded-full bg-blue-400"></div>
<span class="text-slate-500">Experts</span>
</div>
</div>
</div>
<div id="graph-container" class="w-full h-[900px] bg-slate-50 rounded-xl relative">
<!-- Graph Tour Controls -->
<div id="tour-controls" class="absolute top-4 left-4 z-10 flex gap-2" style="display: none;">
<button onclick="previousExpert()" class="bg-white border-2 border-slate-200 px-4 py-2 rounded-lg font-bold text-slate-700 hover:bg-slate-50 transition-all shadow-sm">
<i class="fas fa-chevron-left"></i> Previous
</button>
<button onclick="nextExpert()" class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-indigo-700 transition-all shadow-md">
Next <i class="fas fa-chevron-right"></i>
</button>
<button onclick="toggleAutoTour()" id="auto-tour-btn" class="bg-emerald-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-emerald-700 transition-all shadow-md">
<i class="fas fa-play"></i> Auto-Tour
</button>
<button onclick="stopTour()" class="bg-slate-200 text-slate-700 px-4 py-2 rounded-lg font-bold hover:bg-slate-300 transition-all">
<i class="fas fa-times"></i> Exit Tour
</button>
</div>
</div>
<!-- Graph Controls -->
<div class="mt-4 flex items-center justify-between text-[10px]">
<div class="text-slate-400 font-medium flex items-center gap-4">
<span><i class="fas fa-mouse-pointer mr-1"></i> Click experts for details</span>
<span><i class="fas fa-filter mr-1"></i> Click topics to filter</span>
<span><i class="fas fa-hand-rock mr-1"></i> Drag to reposition</span>
</div>
<div class="flex gap-2">
<button onclick="startTour()" class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-indigo-700 transition-colors">
<i class="fas fa-route mr-1"></i> Start Guided Tour
</button>
<button onclick="resetGraph()" class="bg-slate-100 px-3 py-2 rounded-lg font-bold text-slate-600 hover:bg-slate-200 transition-colors">
Reset View
</button>
</div>
</div>
</div>
<!-- Methodology Card -->
<div class="bg-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-indigo-200 mt-6">
<p class="text-xs font-bold opacity-80 mb-2 uppercase tracking-widest">Why This Visualization?</p>
<h5 class="text-lg font-black italic mb-3">Maps > Lists</h5>
<p class="text-xs leading-relaxed opacity-90 mb-4">
Traditional search engines show flat lists. We show <strong>field topology</strong>—where experts sit in the research landscape, their topic clusters, and collaborative networks.
</p>
<div class="grid grid-cols-2 gap-4 text-xs">
<div>
<p class="font-bold mb-1">Node Size</p>
<p class="opacity-80">Scaled by CPI score</p>
</div>
<div>
<p class="font-bold mb-1">Connections</p>
<p class="opacity-80">Co-authorship + topics</p>
</div>
</div>
</div>
</div>
</div>
{% elif query %}
<!-- Empty State: No Results -->
<div class="bg-white border-2 border-dashed border-slate-200 rounded-[3rem] py-32 text-center">
<div class="max-w-xs mx-auto">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-6 text-slate-300">
<i class="fas fa-search text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-slate-800 mb-2">No experts found</h3>
<p class="text-slate-400 text-sm">No results for "{{ query }}". Try a different research topic.</p>
</div>
</div>
{% else %}
<!-- Empty State: Initial -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all cursor-pointer group">
<div class="w-12 h-12 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center mb-4 group-hover:bg-indigo-600 group-hover:text-white transition-colors">
<i class="fas fa-project-diagram text-xl"></i>
</div>
<h4 class="font-bold text-slate-800 mb-2">Field Topology</h4>
<p class="text-sm text-slate-500">See where experts sit in the research landscape—not just who they are.</p>
</div>
<div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all cursor-pointer group">
<div class="w-12 h-12 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center mb-4 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<i class="fas fa-microscope text-xl"></i>
</div>
<h4 class="font-bold text-slate-800 mb-2">Topic Clustering</h4>
<p class="text-sm text-slate-500">Discover which subfields connect experts and reveal collaboration patterns.</p>
</div>
<div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all cursor-pointer group">
<div class="w-12 h-12 bg-amber-50 text-amber-600 rounded-xl flex items-center justify-center mb-4 group-hover:bg-amber-600 group-hover:text-white transition-colors">
<i class="fas fa-chart-network text-xl"></i>
</div>
<h4 class="font-bold text-slate-800 mb-2">Co-Authorship Graph</h4>
<p class="text-sm text-slate-500">Navigate expertise through collaborative networks—beyond citations.</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- D3.js for Force Graph -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script>
{% if graph_data.nodes %}
// Graph Data from Backend
const graphData = {{ graph_data | tojson }};
// Dimensions
const container = document.getElementById('graph-container');
const width = container.clientWidth;
const height = container.clientHeight;
// Create SVG
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
// Zoom behavior
const zoom = d3.zoom()
.scaleExtent([0.3, 3])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Force simulation with MUCH stronger repulsion
const simulation = d3.forceSimulation(graphData.nodes)
.force('link', d3.forceLink(graphData.links)
.id(d => d.id)
.distance(d => {
// Query to topics: far apart
if (d.source.type === 'query' || d.target.type === 'query') return 200;
// Topics to authors: medium distance
if (d.source.type === 'topic' || d.target.type === 'topic') return 150;
// Co-authors: close together
return 80;
})
.strength(0.3))
.force('charge', d3.forceManyBody()
.strength(d => {
// Query: strong repulsion
if (d.type === 'query') return -3000;
// Topics: medium repulsion
if (d.type === 'topic') return -1500;
// Authors: lighter repulsion
return -800;
}))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => d.size + 15))
.force('x', d3.forceX(width / 2).strength(0.05))
.force('y', d3.forceY(height / 2).strength(0.05));
// Draw links
const link = g.append('g')
.selectAll('line')
.data(graphData.links)
.join('line')
.attr('stroke', d => d.type === 'coauthor' ? '#cbd5e1' : '#e2e8f0')
.attr('stroke-width', d => d.type === 'coauthor' ? 2 : 1)
.attr('stroke-opacity', 0.6);
// Draw nodes
const node = g.append('g')
.selectAll('circle')
.data(graphData.nodes)
.join('circle')
.attr('r', d => d.size)
.attr('fill', d => d.color)
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.attr('id', d => `node-${d.id}`)
.style('cursor', 'pointer')
.call(drag(simulation))
.on('click', (event, d) => {
event.stopPropagation();
if (d.type === 'author') {
showExpertModal(d.id);
} else if (d.type === 'topic') {
filterByTopic(d.name);
}
})
.on('mouseover', function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', d.size * 1.3)
.attr('stroke-width', 4);
tooltip.style('display', 'block')
.html(getTooltipContent(d))
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mouseout', function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', d.size)
.attr('stroke-width', 2);
tooltip.style('display', 'none');
});
// Labels for ALL nodes with smart positioning
const label = g.append('g')
.selectAll('text')
.data(graphData.nodes)
.join('text')
.text(d => {
if (d.type === 'author') {
// Show rank + name for authors
return `#${d.rank} ${d.name.length > 15 ? d.name.substring(0, 15) + '...' : d.name}`;
}
return d.name.length > 25 ? d.name.substring(0, 25) + '...' : d.name;
})
.attr('font-size', d => {
if (d.type === 'query') return 16;
if (d.type === 'topic') return 12;
return d.rank <= 10 ? 10 : 9;
})
.attr('font-weight', d => d.type === 'query' ? 'bold' : d.rank <= 10 ? '600' : 'normal')
.attr('fill', d => {
if (d.type === 'query') return '#0f172a';
if (d.type === 'topic') return '#059669';
return d.rank <= 10 ? '#1e293b' : '#475569';
})
.attr('text-anchor', 'middle')
.attr('dy', d => d.size + 18)
.style('pointer-events', 'none')
.style('user-select', 'none');
// Tooltip
const tooltip = d3.select('body').append('div')
.attr('class', 'graph-tooltip')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'white')
.style('border', '2px solid #e2e8f0')
.style('border-radius', '8px')
.style('padding', '10px')
.style('font-size', '12px')
.style('box-shadow', '0 4px 12px rgba(0,0,0,0.1)')
.style('z-index', '1000')
.style('pointer-events', 'none');
function getTooltipContent(d) {
if (d.type === 'query') return `<strong>Query:</strong> ${d.name}`;
if (d.type === 'topic') return `<strong>Topic:</strong> ${d.name}`;
if (d.type === 'author') {
return `
<strong>${d.name}</strong><br>
<span style="color: #6366f1;">Rank: #${d.rank}</span><br>
CPI: ${d.cpi}<br>
Papers: ${d.papers} | Citations: ${d.citations}
`;
}
}
// Update positions on tick
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y);
});
// Drag behavior
function drag(simulation) {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
// Highlight expert in both table and graph
window.highlightExpert = function(expertId) {
// Reset all nodes
node.attr('stroke', '#fff').attr('stroke-width', 2);
// Highlight selected node
d3.select(`#node-${expertId}`)
.attr('stroke', '#f59e0b')
.attr('stroke-width', 5)
.raise();
// Scroll to expert in table
const expertCard = document.querySelector(`[onclick="highlightExpert('${expertId}')"]`);
if (expertCard) {
expertCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
expertCard.style.borderColor = '#f59e0b';
setTimeout(() => {
expertCard.style.borderColor = '';
}, 2000);
}
};
// Reset graph view
window.resetGraph = function() {
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
);
node.attr('stroke', '#fff').attr('stroke-width', 2);
};
// Show detailed expert modal
window.showExpertModal = function(expertId) {
// Find expert in data
const expert = graphData.nodes.find(n => n.id === expertId);
if (!expert) return;
// Find expert in full list
const experts = {{ experts | tojson }};
const expertData = experts.find(e => e.openalex_id === expertId);
if (!expertData) {
// Fallback: just highlight
highlightExpert(expertId);
return;
}
// Create modal
const modal = document.createElement('div');
modal.id = 'expert-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4px);
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 24px;
max-width: 700px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
padding: 40px;
position: relative;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
">
<button onclick="closeExpertModal()" style="
position: absolute;
top: 20px;
right: 20px;
background: #f1f5f9;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
" onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f1f5f9'">
×
</button>
<h2 style="
font-size: 28px;
font-weight: 900;
color: #0f172a;
margin-bottom: 8px;
">${expertData.name}</h2>
${expertData.institutions && expertData.institutions.length > 0 ? `
<p style="
color: #64748b;
font-size: 14px;
font-style: italic;
margin-bottom: 24px;
">${expertData.institutions.join(' • ')}</p>
` : ''}
<div style="
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
padding: 20px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 16px;
">
<div>
<div style="
font-size: 48px;
font-weight: 900;
color: white;
">${expertData.cpi_score}</div>
<div style="
font-size: 11px;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
text-transform: uppercase;
letter-spacing: 1px;
">CPI Score</div>
</div>
<div style="flex: 1; color: rgba(255, 255, 255, 0.9); font-size: 12px; line-height: 1.6;">
Ranked <strong>#${expertData.cpi_score >= experts[0].cpi_score ? 1 : experts.findIndex(e => e.openalex_id === expertId) + 1}</strong>
in this field based on contribution, proof of work, and impact.
</div>
</div>
<div style="
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 32px;
">
<div style="
background: #f8fafc;
padding: 16px;
border-radius: 12px;
text-align: center;
">
<div style="
font-size: 32px;
font-weight: 900;
color: #0f172a;
">${expertData.h_index}</div>
<div style="
font-size: 10px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
margin-top: 4px;
">h-index</div>
</div>
<div style="
background: #f8fafc;
padding: 16px;
border-radius: 12px;
text-align: center;
">
<div style="
font-size: 32px;
font-weight: 900;
color: #0f172a;
">${expertData.total_citations}</div>
<div style="
font-size: 10px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
margin-top: 4px;
">Citations</div>
</div>
<div style="
background: #f8fafc;
padding: 16px;
border-radius: 12px;
text-align: center;
">
<div style="
font-size: 32px;
font-weight: 900;
color: #0f172a;
">${expertData.total_papers}</div>
<div style="
font-size: 10px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
margin-top: 4px;
">Papers</div>
</div>
</div>
<div style="margin-bottom: 24px;">
<h3 style="
font-size: 12px;
font-weight: 900;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 16px;
">CPI Breakdown</h3>
<div style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 12px; font-weight: 600; color: #64748b;">Contribution</span>
<span style="font-size: 12px; font-weight: 900; color: #6366f1;">${expertData.contribution_score}%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden;">
<div style="
height: 100%;
width: ${expertData.contribution_score}%;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
transition: width 0.5s;
"></div>
</div>
<p style="font-size: 11px; color: #94a3b8; margin-top: 4px;">Recent activity: ${expertData.recent_papers} papers since 2020</p>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 12px; font-weight: 600; color: #64748b;">Proof</span>
<span style="font-size: 12px; font-weight: 900; color: #10b981;">${expertData.proof_score}%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden;">
<div style="
height: 100%;
width: ${expertData.proof_score}%;
background: linear-gradient(90deg, #10b981, #059669);
transition: width 0.5s;
"></div>
</div>
<p style="font-size: 11px; color: #94a3b8; margin-top: 4px;">Depth in this topic</p>
</div>
<div style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 12px; font-weight: 600; color: #64748b;">Impact</span>
<span style="font-size: 12px; font-weight: 900; color: #f59e0b;">${expertData.impact_score}%</span>
</div>
<div style="height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden;">
<div style="
height: 100%;
width: ${expertData.impact_score}%;
background: linear-gradient(90deg, #f59e0b, #d97706);
transition: width 0.5s;
"></div>
</div>
<p style="font-size: 11px; color: #94a3b8; margin-top: 4px;">Citation velocity: ${expertData.citation_velocity}%</p>
</div>
</div>
${expertData.topics && expertData.topics.length > 0 ? `
<div style="margin-bottom: 24px;">
<h3 style="
font-size: 12px;
font-weight: 900;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 12px;
">Research Topics</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
${expertData.topics.map(topic => `
<span style="
background: #f1f5f9;
color: #475569;
padding: 6px 12px;
border-radius: 8px;
font-size: 11px;
font-weight: 600;
">${topic}</span>
`).join('')}
</div>
</div>
` : ''}
${expertData.recent_paper_titles && expertData.recent_paper_titles.length > 0 ? `
<div style="margin-bottom: 24px;">
<h3 style="
font-size: 12px;
font-weight: 900;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 12px;
">Recent Publications</h3>
<div style="space-y: 8px;">
${expertData.recent_paper_titles.slice(0, 3).map((title, idx) => `
<div style="
padding: 12px;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 8px;
">
<div style="
font-size: 10px;
font-weight: 700;
color: #6366f1;
margin-bottom: 4px;
">Paper ${idx + 1}</div>
<div style="
font-size: 12px;
color: #334155;
line-height: 1.5;
">${title}</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<!-- CPI Calculation Explanation Button -->
<button onclick="showCalculationModal('${expertId}')" style="
width: 100%;
background: #f1f5f9;
border: 2px solid #e2e8f0;
color: #475569;
padding: 12px;
border-radius: 12px;
font-weight: 700;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
" onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f1f5f9'">
<i class="fas fa-calculator"></i>
How is this CPI score calculated?
</button>
<div style="
display: flex;
gap: 12px;
margin-top: 32px;
padding-top: 24px;
border-top: 2px solid #f1f5f9;
">
<a href="${expertData.openalex_id}" target="_blank" style="
flex: 1;
background: #6366f1;
color: white;
padding: 14px 24px;
border-radius: 12px;
text-align: center;
text-decoration: none;
font-weight: 700;
font-size: 13px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
" onmouseover="this.style.background='#4f46e5'" onmouseout="this.style.background='#6366f1'">
<i class="fas fa-external-link-alt"></i>
View OpenAlex Profile
</a>
${expertData.orcid ? `
<a href="${expertData.orcid}" target="_blank" style="
flex: 1;
background: #f1f5f9;
color: #475569;
padding: 14px 24px;
border-radius: 12px;
text-align: center;
text-decoration: none;
font-weight: 700;
font-size: 13px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
" onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f1f5f9'">
<i class="fas fa-id-card"></i>
ORCID
</a>
` : ''}
</div>
</div>
`;
document.body.appendChild(modal);
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeExpertModal();
}
});
// Highlight in graph
highlightExpert(expertId);
};
window.closeExpertModal = function() {
const modal = document.getElementById('expert-modal');
if (modal) {
modal.remove();
}
};
// Filter graph by topic
window.filterByTopic = function(topicName) {
// Fade out non-matching nodes
node.style('opacity', d => {
if (d.type === 'query') return 1;
if (d.type === 'topic' && d.name === topicName) return 1;
if (d.type === 'author') {
// Check if this author has this topic
const experts = {{ experts | tojson }};
const expert = experts.find(e => e.openalex_id === d.id);
if (expert && expert.topics && expert.topics.includes(topicName)) {
return 1;
}
}
return 0.15;
});
link.style('opacity', d => {
const sourceMatch = d.source.type === 'topic' && d.source.name === topicName;
const targetMatch = d.target.type === 'topic' && d.target.name === topicName;
return (sourceMatch || targetMatch) ? 0.6 : 0.05;
});
// Show reset button notification
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #6366f1;
color: white;
padding: 12px 20px;
border-radius: 12px;
font-size: 13px;
font-weight: 700;
z-index: 1000;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
`;
notification.innerHTML = `Filtered by: ${topicName} <button onclick="resetFilter()" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 4px 12px;
border-radius: 6px;
margin-left: 8px;
cursor: pointer;
font-weight: 700;
font-size: 11px;
">Reset</button>`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 5000);
};
window.resetFilter = function() {
node.style('opacity', 1);
link.style('opacity', 0.6);
document.querySelectorAll('[style*="position: fixed"]').forEach(el => {
if (el.textContent.includes('Filtered by')) el.remove();
});
};
// Calculation Modal - Shows how CPI is computed
window.showCalculationModal = function(expertId) {
const experts = {{ experts | tojson }};
const expert = experts.find(e => e.openalex_id === expertId);
if (!expert || !expert.cpi_calculation) return;
const calc = expert.cpi_calculation;
const modal = document.createElement('div');
modal.id = 'calculation-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(4px);
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 24px;
max-width: 800px;
width: 90%;
padding: 40px;
position: relative;
">
<button onclick="this.closest('#calculation-modal').remove()" style="
position: absolute;
top: 20px;
right: 20px;
background: #f1f5f9;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
">×</button>
<h2 style="font-size: 24px; font-weight: 900; color: #0f172a; margin-bottom: 8px;">
<i class="fas fa-calculator text-indigo-600 mr-2"></i>
CPI Calculation Breakdown
</h2>
<p style="color: #64748b; font-size: 14px; margin-bottom: 32px;">
For ${expert.name} • Final Score: <strong style="color: #6366f1;">${expert.cpi_score}</strong>
</p>
<!-- Formula Display -->
<div style="
background: linear-gradient(135deg, #6366f1, #8b5cf6);
padding: 24px;
border-radius: 16px;
margin-bottom: 32px;
color: white;
">
<div style="font-size: 12px; opacity: 0.8; margin-bottom: 8px; font-weight: 600;">FORMULA</div>
<div style="font-size: 20px; font-weight: 900; font-family: monospace;">
CPI = ${calc.formula}
</div>
</div>
<!-- Component 1: Contribution -->
<div style="margin-bottom: 24px; padding: 20px; background: #f8fafc; border-radius: 12px;">
<div style="display: flex; justify-content: between; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 900; color: #6366f1;">
Contribution (C)
</h3>
<span style="font-size: 24px; font-weight: 900; color: #6366f1;">
${expert.contribution_score}%
</span>
</div>
<p style="font-size: 13px; color: #64748b; margin-bottom: 12px;">
Measures recent activity relative to career output
</p>
<div style="background: white; padding: 12px; border-radius: 8px; font-size: 12px; color: #334155;">
<strong>Calculation:</strong> Recent papers (${calc.recent_papers_used}) ÷ Total papers (${calc.total_papers_used})<br>
<strong>Result:</strong> ${(calc.contribution_raw * 100).toFixed(1)}% (capped at 100%)<br>
<strong>Weight:</strong> × ${calc.contribution_weight} (30% of final score)
</div>
</div>
<!-- Component 2: Proof -->
<div style="margin-bottom: 24px; padding: 20px; background: #f8fafc; border-radius: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 900; color: #10b981;">
Proof (P)
</h3>
<span style="font-size: 24px; font-weight: 900; color: #10b981;">
${expert.proof_score}%
</span>
</div>
<p style="font-size: 13px; color: #64748b; margin-bottom: 12px;">
Number of works demonstrating expertise in this topic
</p>
<div style="background: white; padding: 12px; border-radius: 8px; font-size: 12px; color: #334155;">
<strong>Calculation:</strong> Total papers (${calc.total_papers_used}) ÷ 10 papers<br>
<strong>Result:</strong> ${(calc.proof_raw * 100).toFixed(1)}% (capped at 100%)<br>
<strong>Weight:</strong> × ${calc.proof_weight} (30% of final score)
</div>
</div>
<!-- Component 3: Impact -->
<div style="margin-bottom: 24px; padding: 20px; background: #f8fafc; border-radius: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 900; color: #f59e0b;">
Impact (I)
</h3>
<span style="font-size: 24px; font-weight: 900; color: #f59e0b;">
${expert.impact_score}%
</span>
</div>
<p style="font-size: 13px; color: #64748b; margin-bottom: 12px;">
Citation count with velocity multiplier (rising vs declining)
</p>
<div style="background: white; padding: 12px; border-radius: 8px; font-size: 12px; color: #334155;">
<strong>Base:</strong> log₁₀(${calc.citations_used} + 1) ÷ 5<br>
<strong>Velocity Boost:</strong> × ${calc.velocity_multiplier.toFixed(2)} (based on ${expert.citation_velocity}% recent citations)<br>
<strong>Result:</strong> ${(calc.impact_raw * 100).toFixed(1)}%<br>
<strong>Weight:</strong> × ${calc.impact_weight} (40% of final score)
</div>
</div>
<!-- Final Calculation -->
<div style="
background: linear-gradient(135deg, #1e293b, #334155);
padding: 20px;
border-radius: 12px;
color: white;
">
<div style="font-size: 14px; font-weight: 900; margin-bottom: 12px;">FINAL CALCULATION</div>
<div style="font-size: 16px; font-family: monospace; line-height: 1.8;">
(${(calc.contribution_raw * 100).toFixed(1)} × 0.3) +
(${(calc.proof_raw * 100).toFixed(1)} × 0.3) +
(${(calc.impact_raw * 100).toFixed(1)} × 0.4) =
<strong style="color: #fbbf24; font-size: 24px;">${expert.cpi_score}</strong>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
};
// Guided Tour System - Now uses ALL experts, not just top 20
let tourState = {
active: false,
currentIndex: 0,
autoPlay: false,
intervalId: null
};
window.startTour = function() {
const experts = {{ all_experts | tojson }}; // Use full list
if (experts.length === 0) return;
tourState.active = true;
tourState.currentIndex = 0;
document.getElementById('tour-controls').style.display = 'flex';
showExpertBubble(experts[0].openalex_id);
zoomToExpert(experts[0].openalex_id);
};
window.nextExpert = function() {
const experts = {{ all_experts | tojson }};
tourState.currentIndex = (tourState.currentIndex + 1) % experts.length;
const expert = experts[tourState.currentIndex];
showExpertBubble(expert.openalex_id);
zoomToExpert(expert.openalex_id);
};
window.previousExpert = function() {
const experts = {{ all_experts | tojson }};
tourState.currentIndex = (tourState.currentIndex - 1 + experts.length) % experts.length;
const expert = experts[tourState.currentIndex];
showExpertBubble(expert.openalex_id);
zoomToExpert(expert.openalex_id);
};
window.toggleAutoTour = function() {
tourState.autoPlay = !tourState.autoPlay;
const btn = document.getElementById('auto-tour-btn');
if (tourState.autoPlay) {
btn.innerHTML = '<i class="fas fa-pause"></i> Pause';
btn.style.background = '#dc2626';
tourState.intervalId = setInterval(nextExpert, 3000); // 3 seconds per expert
} else {
btn.innerHTML = '<i class="fas fa-play"></i> Auto-Tour';
btn.style.background = '#10b981';
clearInterval(tourState.intervalId);
}
};
window.stopTour = function() {
tourState.active = false;
tourState.autoPlay = false;
clearInterval(tourState.intervalId);
document.getElementById('tour-controls').style.display = 'none';
// Remove bubble
const bubble = document.getElementById('expert-bubble');
if (bubble) bubble.remove();
// Reset view
resetGraph();
};
// Show info bubble for expert
function showExpertBubble(expertId) {
// Remove existing bubble
const oldBubble = document.getElementById('expert-bubble');
if (oldBubble) oldBubble.remove();
const experts = {{ all_experts | tojson }};
const expert = experts.find(e => e.openalex_id === expertId);
if (!expert) return;
const rank = experts.indexOf(expert) + 1;
const bubble = document.createElement('div');
bubble.id = 'expert-bubble';
bubble.style.cssText = `
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: white;
border: 3px solid #6366f1;
border-radius: 20px;
padding: 20px 24px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
z-index: 100;
min-width: 450px;
animation: bubbleFadeIn 0.3s ease-out;
`;
bubble.innerHTML = `
<div style="display: flex; align-items: start; gap: 16px;">
<div style="
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 900;
flex-shrink: 0;
">#${rank}</div>
<div style="flex: 1;">
<h3 style="font-size: 18px; font-weight: 900; color: #0f172a; margin-bottom: 4px;">
${expert.name}
</h3>
<p style="font-size: 11px; color: #64748b; font-style: italic; margin-bottom: 12px;">
${expert.institutions && expert.institutions[0] ? expert.institutions[0] : 'Independent Researcher'}
</p>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 12px;">
<div>
<div style="font-size: 20px; font-weight: 900; color: #6366f1;">${expert.cpi_score}</div>
<div style="font-size: 8px; color: #94a3b8; font-weight: 700; text-transform: uppercase;">CPI</div>
</div>
<div>
<div style="font-size: 20px; font-weight: 900; color: #0f172a;">${expert.h_index}</div>
<div style="font-size: 8px; color: #94a3b8; font-weight: 700; text-transform: uppercase;">h-index</div>
</div>
<div>
<div style="font-size: 20px; font-weight: 900; color: #0f172a;">${expert.total_papers}</div>
<div style="font-size: 8px; color: #94a3b8; font-weight: 700; text-transform: uppercase;">Papers</div>
</div>
<div>
<div style="font-size: 20px; font-weight: 900; color: #10b981;">${expert.recent_papers}</div>
<div style="font-size: 8px; color: #94a3b8; font-weight: 700; text-transform: uppercase;">Recent</div>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="showExpertModal('${expertId}')" style="
background: #6366f1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
">Full Profile</button>
<button onclick="showCalculationModal('${expertId}')" style="
background: #f1f5f9;
color: #475569;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
"><i class="fas fa-calculator mr-1"></i>How CPI?</button>
</div>
</div>
</div>
`;
document.getElementById('graph-container').appendChild(bubble);
}
// Zoom to specific expert in graph with smooth animation
function zoomToExpert(expertId) {
const expertNode = graphData.nodes.find(n => n.id === expertId);
if (!expertNode || !expertNode.x || !expertNode.y) {
console.warn(`Node ${expertId} not found or not positioned yet`);
return;
}
// Highlight the node
node.attr('stroke', '#fff').attr('stroke-width', 2);
d3.select(`#node-${CSS.escape(expertId)}`)
.attr('stroke', '#f59e0b')
.attr('stroke-width', 6)
.raise();
// Zoom to node with smooth transition
const scale = 2.0;
const x = width / 2 - expertNode.x * scale;
const y = height / 2 - expertNode.y * scale;
svg.transition()
.duration(1000)
.ease(d3.easeCubicInOut)
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale));
}
// Add CSS animation for bubble
const style = document.createElement('style');
style.textContent = `
@keyframes bubbleFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
`;
document.head.appendChild(style);
{% endif %}
</script>
{% endblock %}